561 Commits

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

3
.gitignore vendored Normal file
View File

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

17
Android/.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/dictionaries
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
/app/debug
/app/release

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

@@ -0,0 +1 @@
MonsterCards

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

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

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

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

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

@@ -0,0 +1,21 @@
<?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="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Android Studio java home" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

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

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

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

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

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

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

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

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

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

4
Android/README.md Normal file
View File

@@ -0,0 +1,4 @@
[![Build status](https://build.appcenter.ms/v0.1/apps/44e4ee45-fe39-4d2d-950f-943e9948ca35/branches/master/badge)](https://appcenter.ms)
# MonsterCards for Android

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

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

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

@@ -0,0 +1,105 @@
plugins {
id 'com.android.application'
id 'androidx.navigation.safeargs'
}
Properties properties = new Properties()
def propertiesFile = project.rootProject.file('local.properties')
if (propertiesFile.exists()) {
properties.load(propertiesFile.newDataInputStream())
}
def appCenterLocalSecret = properties.getProperty('appCenter.localSecret')
def appCenterEnvSecret = System.getenv('APPCENTER_SECRET')
def appCenterSecret = appCenterLocalSecret != null ? appCenterLocalSecret : appCenterEnvSecret != null ? appCenterEnvSecret : ""
android {
compileSdkVersion 31
buildToolsVersion '30.0.3'
defaultConfig {
applicationId "com.majinnaibu.monstercards"
minSdkVersion 22
targetSdkVersion 31
versionCode 1
versionName "1.0"
buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources true
// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
}
lintOptions {
checkDependencies true
}
}
dependencies {
// Included libs
implementation fileTree(dir: "libs", include: ["*.jar"])
// Google
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation "androidx.navigation:navigation-fragment:2.3.5"
implementation "androidx.navigation:navigation-ui:2.3.5"
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Room DB
implementation 'io.reactivex.rxjava3:rxjava:3.1.0'
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation "androidx.room:room-runtime:2.3.0"
annotationProcessor "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-rxjava3:2.3.0"
//testImplementation "androidx.room:room-testing:2.3.0"
// AppCenter
debugImplementation 'com.microsoft.appcenter:appcenter-analytics:4.2.0'
debugImplementation 'com.microsoft.appcenter:appcenter-crashes:4.2.0'
// Flipper
debugImplementation 'com.facebook.flipper:flipper:0.102.0'
debugImplementation "com.facebook.soloader:soloader:0.10.1"
releaseImplementation 'com.facebook.flipper:flipper-noop:0.102.0'
// Other 3rd Party
implementation 'com.atlassian.commonmark:commonmark:0.17.0'
implementation 'com.google.code.gson:gson:2.8.7'
}

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

@@ -0,0 +1,32 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep enum com.majinnaibu.monstercards.data.enums.AbilityScore
-keep enum com.majinnaibu.monstercards.data.enums.ProficiencyType
-keep enum com.majinnaibu.monstercards.data.enums.AdvantageType
-keep enum com.majinnaibu.monstercards.data.enums.TraitType
-keep enum com.majinnaibu.monstercards.data.enums.StringType
-keepclassmembers,allowoptimization enum * {
<fields>;
public static **[] values();
public static ** valueOf(java.lang.String);
}

View File

@@ -0,0 +1,440 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "db1293d2f490940b55ca1f4f56b21b1a",
"entities": [
{
"tableName": "Monster",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `blindsight_range` INTEGER NOT NULL DEFAULT 0, `is_blind_beyond_blindsight_range` INTEGER NOT NULL DEFAULT false, `darkvision_range` INTEGER NOT NULL DEFAULT 0, `tremorsense_range` INTEGER NOT NULL DEFAULT 0, `truesight_range` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `skills` TEXT, `damage_immunities` TEXT, `damage_resistances` TEXT, `damage_vulnerabilities` TEXT, `condition_immunities` TEXT, `languages` TEXT, `abilities` TEXT, `actions` TEXT, `reactions` TEXT, `lair_actions` TEXT, `legendary_actions` TEXT, `regional_actions` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "strengthScore",
"columnName": "strength_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "strengthSavingThrowAdvantage",
"columnName": "strength_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "shieldBonus",
"columnName": "shield_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "naturalArmorBonus",
"columnName": "natural_armor_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "otherArmorDescription",
"columnName": "other_armor_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "hitDice",
"columnName": "hit_dice",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "hasCustomHP",
"columnName": "has_custom_hit_points",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "customHPDescription",
"columnName": "custom_hit_points_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "walkSpeed",
"columnName": "walk_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "burrowSpeed",
"columnName": "burrow_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "climbSpeed",
"columnName": "climb_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "flySpeed",
"columnName": "fly_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "canHover",
"columnName": "can_hover",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "swimSpeed",
"columnName": "swim_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hasCustomSpeed",
"columnName": "has_custom_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "customSpeedDescription",
"columnName": "custom_speed_description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "customProficiencyBonus",
"columnName": "custom_proficiency_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "blindsightRange",
"columnName": "blindsight_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isBlindBeyondBlindsightRange",
"columnName": "is_blind_beyond_blindsight_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "darkvisionRange",
"columnName": "darkvision_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tremorsenseRange",
"columnName": "tremorsense_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "truesightRange",
"columnName": "truesight_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "telepathyRange",
"columnName": "telepathy_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "understandsButDescription",
"columnName": "understands_but_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'db1293d2f490940b55ca1f4f56b21b1a')"
]
}
}

View File

@@ -0,0 +1,499 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "6f1e7a2b2ab96fc4be4da1657a7a0138",
"entities": [
{
"tableName": "monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `blindsight_range` INTEGER NOT NULL DEFAULT 0, `is_blind_beyond_blindsight_range` INTEGER NOT NULL DEFAULT false, `darkvision_range` INTEGER NOT NULL DEFAULT 0, `tremorsense_range` INTEGER NOT NULL DEFAULT 0, `truesight_range` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `skills` TEXT, `damage_immunities` TEXT, `damage_resistances` TEXT, `damage_vulnerabilities` TEXT, `condition_immunities` TEXT, `languages` TEXT, `abilities` TEXT, `actions` TEXT, `reactions` TEXT, `lair_actions` TEXT, `legendary_actions` TEXT, `regional_actions` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "strengthScore",
"columnName": "strength_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "strengthSavingThrowAdvantage",
"columnName": "strength_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "shieldBonus",
"columnName": "shield_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "naturalArmorBonus",
"columnName": "natural_armor_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "otherArmorDescription",
"columnName": "other_armor_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "hitDice",
"columnName": "hit_dice",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "hasCustomHP",
"columnName": "has_custom_hit_points",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "customHPDescription",
"columnName": "custom_hit_points_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "walkSpeed",
"columnName": "walk_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "burrowSpeed",
"columnName": "burrow_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "climbSpeed",
"columnName": "climb_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "flySpeed",
"columnName": "fly_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "canHover",
"columnName": "can_hover",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "swimSpeed",
"columnName": "swim_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hasCustomSpeed",
"columnName": "has_custom_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "customSpeedDescription",
"columnName": "custom_speed_description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "customProficiencyBonus",
"columnName": "custom_proficiency_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "blindsightRange",
"columnName": "blindsight_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isBlindBeyondBlindsightRange",
"columnName": "is_blind_beyond_blindsight_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "darkvisionRange",
"columnName": "darkvision_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "tremorsenseRange",
"columnName": "tremorsense_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "truesightRange",
"columnName": "truesight_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "telepathyRange",
"columnName": "telepathy_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "understandsButDescription",
"columnName": "understands_but_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"ftsVersion": "FTS4",
"ftsOptions": {
"tokenizer": "simple",
"tokenizerArgs": [],
"contentTable": "monsters",
"languageIdColumnName": "",
"matchInfo": "FTS4",
"notIndexedColumns": [],
"prefixSizes": [],
"preferredOrder": "ASC"
},
"contentSyncTriggers": [
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
],
"tableName": "monsters_fts",
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6f1e7a2b2ab96fc4be4da1657a7a0138')"
]
}
}

View File

@@ -0,0 +1,483 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "7c3c3ed79c7002102e7af7cfd21c23e0",
"entities": [
{
"tableName": "monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "strengthScore",
"columnName": "strength_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "strengthSavingThrowAdvantage",
"columnName": "strength_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'none'"
},
{
"fieldPath": "shieldBonus",
"columnName": "shield_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "naturalArmorBonus",
"columnName": "natural_armor_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "otherArmorDescription",
"columnName": "other_armor_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "hitDice",
"columnName": "hit_dice",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "hasCustomHP",
"columnName": "has_custom_hit_points",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "customHPDescription",
"columnName": "custom_hit_points_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "walkSpeed",
"columnName": "walk_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "burrowSpeed",
"columnName": "burrow_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "climbSpeed",
"columnName": "climb_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "flySpeed",
"columnName": "fly_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "canHover",
"columnName": "can_hover",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "swimSpeed",
"columnName": "swim_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hasCustomSpeed",
"columnName": "has_custom_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "customSpeedDescription",
"columnName": "custom_speed_description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "customProficiencyBonus",
"columnName": "custom_proficiency_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "telepathyRange",
"columnName": "telepathy_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "understandsButDescription",
"columnName": "understands_but_description",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "'[]'"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"ftsVersion": "FTS4",
"ftsOptions": {
"tokenizer": "simple",
"tokenizerArgs": [],
"contentTable": "monsters",
"languageIdColumnName": "",
"matchInfo": "FTS4",
"notIndexedColumns": [],
"prefixSizes": [],
"preferredOrder": "ASC"
},
"contentSyncTriggers": [
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
],
"tableName": "monsters_fts",
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7c3c3ed79c7002102e7af7cfd21c23e0')"
]
}
}

View File

@@ -0,0 +1,73 @@
## Monster
id: UUID as TEXT // doesn't exist in the iOS model
abilities: Set<Trait> converted to JSON as TEXT
actions: Set<Trait> converted to JSON as TEXT
alignment: String as TEXT
armor_type: Enum<String> as TEXT
blindsight_range: int as INTEGER
burrow_speed: int as INTEGER
can_hover: boolean as INTEGER
challenge_rating: Enum<String> as TEXT
charisma_saving_throw_advantage
charisma_saving_throw_proficiency
charisma_score: int as INTEGER
climb_speed: int as INTEGER
condition_immunities: Set<String> converted to JSON as TEXT
constitution_saving_throw_advantage
constitution_saving_throw_proficiency
constitution_score: int as INTEGER
//other_armor_description: String as TEXT
custom_challenge_rating_description: String as TEXT
custom_hit_points_description: String
custom_proficiency_bonus: int as INTEGER
custom_speed_description: String as TEXT
damage_immunities: Set<String> converted to JSON as TEXT
damage_resistances: Set<String> converted to JSON as TEXT
damage_vulnerabilities: Set<String> converted to JSON as TEXT
darkvision_range: int as INTEGER
dexterity_saving_throw_advantage
dexterity_saving_throw_proficiency
dexterity_score: int as INTEGER
fly_speed: int as INTEGER
has_custom_hit_points: boolean as INTEGER
has_custom_speed: boolean as INTEGER
// has_shield
hit_dice: int as INTEGER
intelligence_saving_throw_advantage
intelligence_saving_throw_proficiency
intelligence_score: int as INTEGER
is_blind_beyond_blindsight_range: boolean as INTEGER
lair_actions
languages: Set<Language> converted to JSON as TEXT
legendary_actions
name: String as TEXT
natural_armor_bonus: int as INTEGER
other_armor_description: String as TEXT
reactions
regional_actions
// senses
shield_bonus: int as INTEGER
size: String as TEXT
strength_saving_throw_advantage
strength_saving_throw_proficiency
strength_score: int as INTEGER
tag: String as TEXT // subtype || tag
swim_speed: int as INTEGER
telepathy_range: int as INTEGER
tremorsense_range: int as INTEGER
truesight_range: int as INTEGER
type: String as TEXT
understands_but_description: String as TEXT
walk_speed: int as INTEGER
wisdom_saving_throw_advantage
wisdom_saving_throw_proficiency
wisdom_score: int as INTEGER
// tracked as relationship (don't do this)
skills: Set<Skill> converted to JSON as TEXT
## Skill
// ability_score_name String defaults to "strength"
// advantage String defaults to "none"
// name String defaults to ""
// proficiency String defaults to "none"

View File

@@ -0,0 +1,26 @@
package com.majinnaibu.monstercards;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.majinnaibu.monstercards", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,22 @@
package com.majinnaibu.monstercards.init;
import android.app.Application;
import com.majinnaibu.monstercards.BuildConfig;
import com.microsoft.appcenter.AppCenter;
import com.microsoft.appcenter.analytics.Analytics;
import com.microsoft.appcenter.crashes.Crashes;
public class AppCenterInitializer {
public static void init(Application app) {
if (BuildConfig.APPCENTER_SECRET != null && !"".equals(BuildConfig.APPCENTER_SECRET)) {
AppCenter.start(
app,
BuildConfig.APPCENTER_SECRET,
Analytics.class,
Crashes.class
);
}
}
}

View File

@@ -0,0 +1,42 @@
package com.majinnaibu.monstercards.init;
import android.content.Context;
import android.os.Bundle;
import androidx.navigation.NavController;
import androidx.navigation.NavDestination;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.soloader.SoLoader;
import com.google.gson.Gson;
import com.majinnaibu.monstercards.BuildConfig;
public class FlipperInitializer {
public static void init(Context ctx) {
SoLoader.init(ctx, false);
if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(ctx)) {
final FlipperClient client = AndroidFlipperClient.getInstance(ctx);
client.addPlugin(new InspectorFlipperPlugin(ctx, DescriptorMapping.withDefaults()));
client.addPlugin(new DatabasesFlipperPlugin(ctx));
client.addPlugin(new SharedPreferencesFlipperPlugin(ctx));
client.addPlugin(NavigationFlipperPlugin.getInstance());
client.start();
}
}
public static void sendNavigationEvent(NavController controller, NavDestination destination, Bundle arguments) {
Gson gson = new Gson();
String json = gson.toJson(arguments != null ? arguments : new Bundle());
NavigationFlipperPlugin.getInstance().sendNavigationEvent(String.format("%s:%s", destination.getLabel(), json), null, null);
}
}

View File

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

View File

@@ -0,0 +1,30 @@
package com.majinnaibu.monstercards;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import com.majinnaibu.monstercards.data.MonsterDAO;
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
import com.majinnaibu.monstercards.data.converters.ListOfTraitsConverter;
import com.majinnaibu.monstercards.data.converters.SetOfLanguageConverter;
import com.majinnaibu.monstercards.data.converters.SetOfSkillConverter;
import com.majinnaibu.monstercards.data.converters.SetOfStringConverter;
import com.majinnaibu.monstercards.data.converters.UUIDConverter;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.MonsterFTS;
@Database(entities = {Monster.class, MonsterFTS.class}, version = 3)
@TypeConverters({
ArmorTypeConverter.class,
ChallengeRatingConverter.class,
ListOfTraitsConverter.class,
SetOfLanguageConverter.class,
SetOfSkillConverter.class,
SetOfStringConverter.class,
UUIDConverter.class,
})
public abstract class AppDatabase extends RoomDatabase {
public abstract MonsterDAO monsterDAO();
}

View File

@@ -0,0 +1,156 @@
package com.majinnaibu.monstercards;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.init.AppCenterInitializer;
import com.majinnaibu.monstercards.init.FlipperInitializer;
import com.majinnaibu.monstercards.utils.Logger;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Objects;
public class MainActivity extends AppCompatActivity {
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
getOnBackPressedDispatcher().onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@SuppressWarnings("ConstantConditions")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCenterInitializer.init(getApplication());
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_search,
R.id.navigation_dashboard,
R.id.navigation_collections,
R.id.navigation_library)
.build();
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
navController.addOnDestinationChangedListener(FlipperInitializer::sendNavigationEvent);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView, navController);
onNewIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
String json = readMonsterJSONFromIntent(intent);
if (!StringHelper.isNullOrEmpty(json)) {
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
NavController navController = navHostFragment.getNavController();
NavDirections action = MobileNavigationDirections.actionGlobalMonsterImportFragment(json);
navController.navigate(action);
}
}
@Nullable
private String readMonsterJSONFromIntent(@NonNull Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
String type = intent.getType();
String json;
Uri uri = null;
if ("android.intent.action.MAIN".equals(action)) {
return null;
} else if ("android.intent.action.SEND".equals(action) && "text/plain".equals(type)) {
uri = extras.getParcelable("android.intent.extra.STREAM");
} else if ("android.intent.action.VIEW".equals(action) && ("text/plain".equals(type) || "application/octet-stream".equals(type))) {
uri = intent.getData();
} else {
Logger.logError(String.format("unexpected launch configuration action: %s, type: %s", action, type));
}
if (uri == null) {
return null;
}
json = readContentsOfUri(uri);
if (StringHelper.isNullOrEmpty(json)) {
return null;
}
return json;
}
@Nullable
private String readContentsOfUri(Uri uri) {
StringBuilder builder = new StringBuilder();
try (
InputStream inputStream = openInputStream(uri);
BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream)))
) {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} catch (IOException e) {
Logger.logError("error reading file", e);
return null;
}
return builder.toString();
}
private boolean isVirtualFile(Uri uri) {
if (!DocumentsContract.isDocumentUri(this, uri)) {
return false;
}
Cursor cursor = getContentResolver().query(
uri,
new String[]{DocumentsContract.Document.COLUMN_FLAGS},
null, null, null);
int flags = 0;
if (cursor.moveToFirst()) {
flags = cursor.getInt(0);
}
cursor.close();
return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}
private InputStream openInputStream(Uri uri) throws IOException {
ContentResolver resolver = getContentResolver();
if (isVirtualFile(uri)) {
String[] openableMimeTypes = resolver.getStreamTypes(uri, "*/*");
if (openableMimeTypes == null || openableMimeTypes.length <= 0) {
throw new FileNotFoundException();
}
return resolver.openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null).createInputStream();
} else {
return resolver.openInputStream(uri);
}
}
}

View File

@@ -0,0 +1,82 @@
package com.majinnaibu.monstercards;
import android.app.Application;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.init.FlipperInitializer;
public class MonsterCardsApplication extends Application {
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// rename table monster to monsters
database.execSQL("ALTER TABLE monster RENAME TO monsters");
// create the fts view
database.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)");
// build the initial full text search index
database.execSQL("INSERT INTO monsters_fts(monsters_fts) VALUES('rebuild')");
}
};
private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// Add the senses column
database.execSQL("ALTER TABLE monsters ADD COLUMN 'senses' TEXT DEFAULT '[]'");
database.execSQL("CREATE TABLE new_monsters (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))");
database.execSQL("INSERT INTO new_monsters(id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions) SELECT id, name, size, type, subtype, alignment, strength_score, strength_saving_throw_advantage, strength_saving_throw_proficiency, dexterity_score, dexterity_saving_throw_advantage, dexterity_saving_throw_proficiency, constitution_score, constitution_saving_throw_advantage, constitution_saving_throw_proficiency, intelligence_score, intelligence_saving_throw_advantage, intelligence_saving_throw_proficiency, wisdom_score, wisdom_saving_throw_advantage, wisdom_saving_throw_proficiency, charisma_score, charisma_saving_throw_advantage, charisma_saving_throw_proficiency, armor_type, shield_bonus, natural_armor_bonus, other_armor_description, hit_dice, has_custom_hit_points, custom_hit_points_description, walk_speed, burrow_speed, climb_speed, fly_speed, can_hover, swim_speed, has_custom_speed, custom_speed_description, challenge_rating, custom_challenge_rating_description, custom_proficiency_bonus, telepathy_range, understands_but_description, senses, skills, damage_immunities, damage_resistances, damage_vulnerabilities, condition_immunities, languages, abilities, actions, reactions, lair_actions, legendary_actions, regional_actions FROM monsters");
database.execSQL("DROP TABLE monsters");
database.execSQL("ALTER TABLE new_monsters RENAME TO monsters");
}
};
private MonsterRepository m_monsterLibraryRepository;
public MonsterCardsApplication() {
}
public MonsterRepository getMonsterRepository() {
return m_monsterLibraryRepository;
}
// Called when the application is starting, before any other application objects have been created.
// Overriding this method is totally optional!
@Override
public void onCreate() {
super.onCreate();
// Required initialization logic here!
FlipperInitializer.init(this);
// .fallbackToDestructiveMigration()
AppDatabase m_db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "monsters")
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
// .fallbackToDestructiveMigration()
.build();
m_monsterLibraryRepository = new MonsterRepository(m_db);
}
// Called by the system when the device configuration changes while your component is running.
// Overriding this method is totally optional!
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
// This is called when the overall system is running low on memory,
// and would like actively running processes to tighten their belts.
// Overriding this method is totally optional!
@Override
public void onLowMemory() {
super.onLowMemory();
}
}

View File

@@ -0,0 +1,113 @@
package com.majinnaibu.monstercards.data;
import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ArmorType;
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait;
@SuppressWarnings("unused")
public final class DevContent {
@NonNull
public static Monster createSampleMonster() {
Monster monster = new Monster();
// Name
monster.name = "Pixie";
// Meta
monster.size = "tiny";
monster.type = "fey";
monster.subtype = "";
monster.alignment = "neutral good";
monster.armorType = ArmorType.NONE;
// Armor & Armor Class
monster.shieldBonus = 0;
monster.naturalArmorBonus = 7;
monster.otherArmorDescription = "14";
// Hit Points
monster.hitDice = 1;
monster.hasCustomHP = false;
monster.customHPDescription = "11 (2d8 + 2)";
monster.walkSpeed = 10;
monster.burrowSpeed = 0;
monster.climbSpeed = 0;
monster.flySpeed = 30;
monster.canHover = false;
monster.swimSpeed = 0;
monster.hasCustomSpeed = false;
monster.customSpeedDescription = "30 ft., swim 30 ft.";
// Ability Scores
monster.strengthScore = Integer.parseInt("2");
monster.dexterityScore = Integer.parseInt("20");
monster.constitutionScore = Integer.parseInt("8");
monster.intelligenceScore = Integer.parseInt("10");
monster.wisdomScore = Integer.parseInt("14");
monster.charismaScore = Integer.parseInt("15");
// monster.strengthScore = 10;
// monster.dexterityScore = 10;
// monster.constitutionScore = 10;
// monster.intelligenceScore = 10;
// monster.wisdomScore = 10;
// monster.charismaScore = 10;
// Saving Throws
monster.strengthSavingThrowAdvantage = AdvantageType.NONE;
monster.strengthSavingThrowProficiency = ProficiencyType.NONE;
monster.dexteritySavingThrowAdvantage = AdvantageType.ADVANTAGE;
monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
monster.constitutionSavingThrowAdvantage = AdvantageType.DISADVANTAGE;
monster.constitutionSavingThrowProficiency = ProficiencyType.EXPERTISE;
monster.intelligenceSavingThrowAdvantage = AdvantageType.NONE;
monster.intelligenceSavingThrowProficiency = ProficiencyType.EXPERTISE;
monster.wisdomSavingThrowAdvantage = AdvantageType.ADVANTAGE;
monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
monster.charismaSavingThrowAdvantage = AdvantageType.DISADVANTAGE;
monster.charismaSavingThrowProficiency = ProficiencyType.NONE;
//Skills
monster.skills.add(new Skill("perception", AbilityScore.WISDOM));
monster.skills.add(new Skill("stealth", AbilityScore.DEXTERITY));
// Damage Types
monster.damageImmunities.add("force");
monster.damageImmunities.add("lightning");
monster.damageImmunities.add("poison");
monster.damageResistances.add("cold");
monster.damageResistances.add("fire");
monster.damageResistances.add("piercing");
monster.damageVulnerabilities.add("acid");
monster.damageVulnerabilities.add("bludgeoning");
monster.damageVulnerabilities.add("necrotic");
// Condition Immunities
monster.conditionImmunities.add("blinded");
// Senses
monster.senses.add("blindsight 10 ft. (blind beyond this range)");
monster.senses.add("darkvision 20 ft.");
monster.senses.add("tremorsense 30 ft.");
monster.senses.add("truesight 40 ft.");
monster.telepathyRange = 20;
monster.understandsButDescription = "doesn't care";
// Languages
monster.languages.add(new Language("English", true));
monster.languages.add(new Language("Steve", false));
monster.languages.add(new Language("Spanish", true));
monster.languages.add(new Language("French", true));
monster.languages.add(new Language("Mermataur", false));
monster.languages.add(new Language("Goldfish", false));
// Challenge Rating
monster.challengeRating = ChallengeRating.CUSTOM;
monster.customChallengeRatingDescription = "Infinite (0XP)";
monster.customProficiencyBonus = 4;
// Abilities
monster.abilities.add(new Trait("Spellcasting", "The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC [WIS SAVE], [WIS ATK] to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n\n> Cantrips (at will): _light, sacred flame, thaumaturgy_\n> 1st level (3 slots): _bless, cure wounds, sanctuary_"));
monster.abilities.add(new Trait("Amphibious", "The dragon can breathe air and water."));
monster.abilities.add(new Trait("Legendary Resistance (3/Day)", "If the dragon fails a saving throw, it can choose to succeed instead."));
// Actions
monster.actions.add(new Trait("Club", "_Melee Weapon Attack:_ [STR ATK] to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage."));
return monster;
}
}

View File

@@ -0,0 +1,39 @@
package com.majinnaibu.monstercards.data;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.majinnaibu.monstercards.models.Monster;
import java.util.List;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
@Dao
public interface MonsterDAO {
@Query("SELECT * FROM monsters")
Flowable<List<Monster>> getAll();
@Query("SELECT * FROM monsters WHERE id IN (:monsterIds)")
Flowable<List<Monster>> loadAllByIds(String[] monsterIds);
@Query("SELECT * FROM monsters WHERE name LIKE :name LIMIT 1")
Flowable<Monster> findByName(String name);
@Query("SELECT monsters.* FROM monsters JOIN monsters_fts ON monsters.oid = monsters_fts.docid WHERE monsters_fts MATCH :searchText")
Flowable<List<Monster>> search(String searchText);
@Insert
Completable insertAll(Monster... monsters);
@Insert(onConflict = OnConflictStrategy.REPLACE)
Completable save(Monster... monsters);
@Delete
Completable delete(Monster monster);
}

View File

@@ -0,0 +1,112 @@
package com.majinnaibu.monstercards.data;
import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.AppDatabase;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Monster;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.schedulers.Schedulers;
@SuppressWarnings("ResultOfMethodCallIgnored")
public class MonsterRepository {
private final AppDatabase m_db;
public MonsterRepository(@NonNull AppDatabase db) {
m_db = db;
}
public Flowable<List<Monster>> getMonsters() {
return m_db.monsterDAO()
.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<Monster>> searchMonsters(String searchText) {
return m_db.monsterDAO()
.getAll()
.map(monsters -> {
ArrayList<Monster> filteredMonsters = new ArrayList<>();
for (Monster monster : monsters) {
if (Helpers.monsterMatchesSearch(monster, searchText)) {
filteredMonsters.add(monster);
}
}
return (List<Monster>) filteredMonsters;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<Monster> getMonster(@NonNull UUID monsterId) {
return m_db.monsterDAO()
.loadAllByIds(new String[]{monsterId.toString()})
.map(
monsters -> {
if (monsters.size() > 0) {
return monsters.get(0);
} else {
return null;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Completable addMonster(Monster monster) {
Completable result = m_db.monsterDAO().insertAll(monster);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable deleteMonster(Monster monster) {
Completable result = m_db.monsterDAO().delete(monster);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
public Completable saveMonster(Monster monster) {
Completable result = m_db.monsterDAO().save(monster);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
private static class Helpers {
static boolean monsterMatchesSearch(Monster monster, String searchText) {
if (StringHelper.isNullOrEmpty(searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.name, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.size, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.type, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.subtype, searchText)) {
return true;
}
if (StringHelper.containsCaseInsensitive(monster.alignment, searchText)) {
return true;
}
return false;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.majinnaibu.monstercards.models.Trait;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class ListOfTraitsConverter {
@TypeConverter
public static String fromListOfTraits(List<Trait> traits) {
Gson gson = new Gson();
return gson.toJson(traits);
}
@TypeConverter
public static List<Trait> listOfTraitsFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<ArrayList<Trait>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -0,0 +1,28 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.majinnaibu.monstercards.models.Language;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Set;
public class SetOfLanguageConverter {
@TypeConverter
public static String fromSetOfLanguage(Set<Language> languages) {
Gson gson = new Gson();
return gson.toJson(languages);
}
@TypeConverter
public static Set<Language> setOfLanguageFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<HashSet<Language>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -0,0 +1,28 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.majinnaibu.monstercards.models.Skill;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Set;
public class SetOfSkillConverter {
@TypeConverter
public static String fromSetOfSkill(Set<Skill> skills) {
Gson gson = new Gson();
return gson.toJson(skills);
}
@TypeConverter
public static Set<Skill> setOfSkillFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<HashSet<Skill>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.monstercards.data.converters;
import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Set;
public class SetOfStringConverter {
@TypeConverter
public static String fromSetOfString(Set<String> strings) {
Gson gson = new Gson();
return gson.toJson(strings);
}
@TypeConverter
public static Set<String> setOfStringFromString(String string) {
Gson gson = new Gson();
Type setType = new TypeToken<HashSet<String>>() {
}.getType();
return gson.fromJson(string, setType);
}
}

View File

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

View File

@@ -0,0 +1,31 @@
package com.majinnaibu.monstercards.data.enums;
@SuppressWarnings("unused")
public enum AbilityScore {
STRENGTH("strength", "Strength", "STR"),
DEXTERITY("dexterity", "Dexterity", "DEX"),
CONSTITUTION("constitution", "Constitution", "CON"),
INTELLIGENCE("intelligence", "Intelligence", "INT"),
WISDOM("wisdom", "Wisdom", "WIS"),
CHARISMA("charisma", "Charisma", "CHA"),
;
public final String displayName;
public final String shortDisplayName;
public final String stringValue;
AbilityScore(String stringValue, String displayName, String shortDisplayName) {
this.displayName = displayName;
this.stringValue = stringValue;
this.shortDisplayName = shortDisplayName;
}
public static AbilityScore valueOfString(String string) {
for (AbilityScore abilityScore : values()) {
if (abilityScore.stringValue.equals(string)) {
return abilityScore;
}
}
return AbilityScore.STRENGTH;
}
}

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.monstercards.data.enums;
public enum AdvantageType {
NONE("none", "None", ""),
ADVANTAGE("advantage", "Advantage", "A"),
DISADVANTAGE("disadvantage", "Disadvantage", "D"),
;
public final String displayName;
public final String stringValue;
public final String label;
AdvantageType(String stringValue, String displayName, String label) {
this.displayName = displayName;
this.stringValue = stringValue;
this.label = label;
}
public static AdvantageType valueOfString(String string) {
for (AdvantageType advantageType : values()) {
if (advantageType.stringValue.equals(string)) {
return advantageType;
}
}
return AdvantageType.NONE;
}
}

View File

@@ -0,0 +1,41 @@
package com.majinnaibu.monstercards.data.enums;
@SuppressWarnings("unused")
public enum ArmorType {
NONE("none", "None", 10),
NATURAL_ARMOR("natural armor", "Natural Armor", 10),
MAGE_ARMOR("mage armor", "Mage Armor", 10),
PADDED("padded", "Padded", 11),
LEATHER("leather", "Leather", 11),
STUDDED_LEATHER("studded", "Studded Leather", 12),
HIDE("hide", "Hide", 12),
CHAIN_SHIRT("chain shirt", "Chain Shirt", 13),
SCALE_MAIL("scale mail", "Scale Mail", 14),
BREASTPLATE("breastplate", "Breastplate", 14),
HALF_PLATE("half plate", "Half Plate", 15),
RING_MAIL("ring mail", "Ring Mail", 14),
CHAIN_MAIL("chain mail", "Chain Mail", 16),
SPLINT_MAIL("splint", "Splint Mail", 17),
PLATE_MAIL("plate", "Plate Mail", 18),
OTHER("other", "Other", 10),
;
public final String displayName;
public final String stringValue;
public final int baseArmorClass;
ArmorType(String stringValue, String displayName, int baseArmorClass) {
this.displayName = displayName;
this.stringValue = stringValue;
this.baseArmorClass = baseArmorClass;
}
public static ArmorType valueOfString(String string) {
for (ArmorType armorType : values()) {
if (armorType.stringValue.equals(string)) {
return armorType;
}
}
return ArmorType.NONE;
}
}

View File

@@ -0,0 +1,60 @@
package com.majinnaibu.monstercards.data.enums;
@SuppressWarnings("unused")
public enum ChallengeRating {
CUSTOM("custom", "Custom", 0),
ZERO("zero", "0 (10 XP)", 2),
ONE_EIGHTH("1/8", "1/8 (25 XP)", 2),
ONE_QUARTER("1/4", "1/4 (50 XP)", 2),
ONE_HALF("1/2", "1/2 (100 XP)", 2),
ONE("1", "1 (200 XP)", 2),
TWO("2", "2 (450 XP)", 2),
THREE("3", "3 (700 XP)", 2),
FOUR("4", "4 (1,100 XP)", 2),
FIVE("5", "5 (1,800 XP)", 3),
SIX("6", "6 (2,300 XP)", 3),
SEVEN("7", "7 (2,900 XP)", 3),
EIGHT("8", "8 (3,900 XP)", 3),
NINE("9", "9 (5,000 XP)", 4),
TEN("10", "10 (5,900 XP)", 4),
ELEVEN("11", "11 (7,200 XP)", 4),
TWELVE("12", "12 (8,400 XP)", 4),
THIRTEEN("13", "13 (10,000 XP)", 5),
FOURTEEN("14", "14 (11,500 XP)", 5),
FIFTEEN("15", "15 (13,000 XP)", 5),
SIXTEEN("16", "16 (15,000 XP)", 5),
SEVENTEEN("17", "17 (18,000 XP)", 6),
EIGHTEEN("18", "18 (20,000 XP)", 6),
NINETEEN("19", "19 (22,000 XP)", 6),
TWENTY("20", "20 (25,000 XP)", 6),
TWENTY_ONE("21", "21 (33,000 XP)", 7),
TWENTY_TWO("22", "22 (41,000 XP)", 7),
TWENTY_THREE("23", "23 (50,000 XP)", 7),
TWENTY_FOUR("24", "24 (62,000 XP)", 7),
TWENTY_FIVE("25", "25 (75,000 XP)", 8),
TWENTY_SIX("26", "26 (90,000 XP)", 8),
TWENTY_SEVEN("27", "27 (105,000 XP)", 8),
TWENTY_EIGHT("28", "28 (120,000 XP)", 8),
TWENTY_NINE("29", "29 (135,000 XP)", 9),
THIRTY("30", "30 (155,000 XP)", 9),
;
public final String displayName;
public final String stringValue;
public final int proficiencyBonus;
ChallengeRating(String stringValue, String displayName, int proficiencyBonus) {
this.displayName = displayName;
this.stringValue = stringValue;
this.proficiencyBonus = proficiencyBonus;
}
public static ChallengeRating valueOfString(String string) {
for (ChallengeRating challengeRating : values()) {
if (challengeRating.stringValue.equals(string)) {
return challengeRating;
}
}
return ChallengeRating.ONE;
}
}

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.monstercards.data.enums;
public enum ProficiencyType {
NONE("none", "None", ""),
PROFICIENT("proficient", "Proficient", "P"),
EXPERTISE("expertise", "Expertise", "Ex"),
;
public final String displayName;
public final String stringValue;
public final String label;
ProficiencyType(String stringValue, String displayName, String label) {
this.displayName = displayName;
this.stringValue = stringValue;
this.label = label;
}
public static ProficiencyType valueOfString(String string) {
for (ProficiencyType proficiencyType : values()) {
if (proficiencyType.stringValue.equals(string)) {
return proficiencyType;
}
}
return ProficiencyType.NONE;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.monstercards.helpers;
import org.commonmark.node.Document;
import org.commonmark.node.Node;
import org.commonmark.node.Paragraph;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
public final class CommonMarkHelper {
public static String toHtml(String rawCommonMark) {
Parser parser = Parser.builder().build();
Node document = parser.parse(rawCommonMark);
Node parent1 = document.getFirstChild();
Node parent2 = document.getLastChild();
if (parent1 == parent2 && parent1 instanceof Paragraph) {
document = new Document();
Node child = parent1.getFirstChild();
while (child != null) {
Node nextChild = child.getNext();
document.appendChild(child);
child = nextChild;//child.getNext();
}
}
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
}
}

View File

@@ -0,0 +1,311 @@
package com.majinnaibu.monstercards.helpers;
import androidx.annotation.NonNull;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.models.Trait;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public class MonsterImportHelper {
@NonNull
public static Monster fromJSON(String json) {
JsonObject rootDict = JsonParser.parseString(json).getAsJsonObject();
Monster monster = new Monster();
monster.name = Helpers.getString(rootDict, "name");
monster.size = Helpers.getString(rootDict, "size");
monster.type = Helpers.getString(rootDict, "type");
monster.subtype = Helpers.getString(rootDict, "tag");
monster.alignment = Helpers.getString(rootDict, "alignment");
monster.hitDice = Helpers.getInt(rootDict, "hitDice");
monster.armorType = ArmorTypeConverter.armorTypeFromStringValue(Helpers.getString(rootDict, "armorName"));
monster.shieldBonus = Helpers.getInt(rootDict, "shieldBonus");
monster.naturalArmorBonus = Helpers.getInt(rootDict, "natArmorBonus");
monster.otherArmorDescription = Helpers.getString(rootDict, "otherArmorDesc");
monster.walkSpeed = Helpers.getInt(rootDict, "speed");
monster.burrowSpeed = Helpers.getInt(rootDict, "burrowSpeed");
monster.climbSpeed = Helpers.getInt(rootDict, "climbSpeed");
monster.flySpeed = Helpers.getInt(rootDict, "flySpeed");
monster.canHover = Helpers.getBool(rootDict, "hover");
monster.swimSpeed = Helpers.getInt(rootDict, "swimSpeed");
monster.hasCustomHP = Helpers.getBool(rootDict, "customHP");
monster.hasCustomSpeed = Helpers.getBool(rootDict, "customSpeed");
monster.customHPDescription = Helpers.getString(rootDict, "hpText");
monster.customSpeedDescription = Helpers.getString(rootDict, "speedDesc");
monster.strengthScore = Helpers.getInt(rootDict, "strPoints");
monster.dexterityScore = Helpers.getInt(rootDict, "dexPoints");
monster.constitutionScore = Helpers.getInt(rootDict, "conPoints");
monster.intelligenceScore = Helpers.getInt(rootDict, "intPoints");
monster.wisdomScore = Helpers.getInt(rootDict, "wisPoints");
monster.charismaScore = Helpers.getInt(rootDict, "chaPoints");
Helpers.addSense(monster, rootDict, "blindsight");
// Helpers.getBool(rootDict, "blind");
Helpers.addSense(monster, rootDict, "darkvision");
Helpers.addSense(monster, rootDict, "tremorsense");
Helpers.addSense(monster, rootDict, "truesight");
monster.telepathyRange = Helpers.getInt(rootDict, "telepathy");
monster.challengeRating = ChallengeRatingConverter.challengeRatingFromStringValue(Helpers.getString(rootDict, "cr"));
monster.customChallengeRatingDescription = Helpers.getString(rootDict, "customCr");
monster.customProficiencyBonus = Helpers.getInt(rootDict, "customProf");
// Helpers.getBool(rootDict, "isLegendary");
// Helpers.getString(rootDict, "legendariesDescription");
// Helpers.getBool(rootDict, "isLair");
// Helpers.getString(rootDict, "lairDescription");
// Helpers.getString(rootDict, "lairDescriptionEnd");
// Helpers.getBool(rootDict, "isRegional");
// Helpers.getString(rootDict, "regionalDescription");
// Helpers.getString(rootDict, "regionalDescriptionEnd");
// properties: []
monster.abilities = Helpers.getListOfTraits(rootDict, "abilities");
monster.actions = Helpers.getListOfTraits(rootDict, "actions");
monster.reactions = Helpers.getListOfTraits(rootDict, "reactions");
monster.legendaryActions = Helpers.getListOfTraits(rootDict, "legendaries");
monster.lairActions = Helpers.getListOfTraits(rootDict, "lairs");
monster.regionalActions = Helpers.getListOfTraits(rootDict, "regionals");
Helpers.addSavingThrows(monster, rootDict);
// skills: []
monster.skills = Helpers.getSetOfSkills(rootDict);
// damagetypes: []
// specialdamage: []
monster.damageImmunities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "i");
monster.damageImmunities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "i"));
monster.damageResistances = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "r");
monster.damageResistances.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "r"));
monster.damageVulnerabilities = Helpers.getSetOfDamageTypes(rootDict, "damageTypes", "v");
monster.damageVulnerabilities.addAll(Helpers.getSetOfDamageTypes(rootDict, "specialdamage", "v"));
// conditions: []
monster.conditionImmunities = Helpers.getSetOfDamageTypes(rootDict, "conditions");
// languages: []
monster.languages = Helpers.getSetOfLanguages(rootDict, "languages");
// understandsBut: ""
monster.understandsButDescription = Helpers.getString(rootDict, "understandsBut");
// shortName: ""
// doubleColumns: true
// separationPoint: -1
// damage: []
// pluralName: ""
return monster;
}
public static class Helpers {
public static String getString(JsonObject dict, String name) {
return getString(dict, name, "");
}
public static String getString(@NonNull JsonObject dict, String name, String defaultValue) {
if (dict.has(name)) {
return dict.get(name).getAsString();
}
return defaultValue;
}
public static int getInt(JsonObject dict, String name) {
return getInt(dict, name, 0);
}
public static int getInt(@NonNull JsonObject dict, String name, int defaultValue) {
if (dict.has(name)) {
JsonElement element = dict.get(name);
if (element.isJsonPrimitive()) {
JsonPrimitive rawValue = element.getAsJsonPrimitive();
if (rawValue.isNumber()) {
return rawValue.getAsInt();
} else {
try {
return rawValue.getAsInt();
} catch (Exception ex) {
return defaultValue;
}
}
}
}
return defaultValue;
}
public static boolean getBool(JsonObject dict, String name) {
return getBool(dict, name, false);
}
public static boolean getBool(@NonNull JsonObject dict, String name, boolean defaultValue) {
if (dict.has(name)) {
JsonElement element = dict.get(name);
if (element.isJsonPrimitive()) {
JsonPrimitive rawValue = element.getAsJsonPrimitive();
if (rawValue.isBoolean()) {
return rawValue.getAsBoolean();
} else {
try {
return rawValue.getAsBoolean();
} catch (Exception ex) {
return defaultValue;
}
}
}
}
return defaultValue;
}
@NonNull
public static String formatDistance(String name, int distance) {
// TODO: consider moving this to a string resource so it can be localized
return String.format(Locale.getDefault(), "%s %d ft.", name, distance);
}
public static void addSense(Monster monster, JsonObject root, String name) {
int distance = Helpers.getInt(root, name);
if (distance > 0) {
monster.senses.add(Helpers.formatDistance(name, distance));
}
}
@NonNull
public static List<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 = Helpers.getString(jsonObject, "name");
String description = Helpers.getString(jsonObject, "desc");
Trait trait = new Trait(traitName, description);
traits.add(trait);
}
}
}
}
return traits;
}
public static void addSavingThrows(Monster monster, @NonNull JsonObject root) {
if (root.has("sthrows")) {
JsonElement arrayElement = root.get("sthrows");
if (arrayElement.isJsonArray()) {
JsonArray array = arrayElement.getAsJsonArray();
int size = array.size();
for (int index = 0; index < size; index++) {
JsonElement jsonElement = array.get(index);
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
String name = Helpers.getString(jsonObject, "name");
if ("str".equals(name)) {
monster.strengthSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("dex".equals(name)) {
monster.dexteritySavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("con".equals(name)) {
monster.constitutionSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("int".equals(name)) {
monster.intelligenceSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("wis".equals(name)) {
monster.wisdomSavingThrowProficiency = ProficiencyType.PROFICIENT;
} else if ("cha".equals(name)) {
monster.charismaSavingThrowProficiency = ProficiencyType.PROFICIENT;
}
}
}
}
}
}
@NonNull
public static Set<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 = Helpers.getString(jsonObject, "name");
String stat = Helpers.getString(jsonObject, "stat");
String note = Helpers.getString(jsonObject, "note");
Skill skill = new Skill(name, AbilityScore.valueOfString(stat), AdvantageType.NONE, " (ex)".equals(note) ? ProficiencyType.EXPERTISE : ProficiencyType.PROFICIENT);
skills.add(skill);
}
}
}
}
return skills;
}
@NonNull
public static Set<String> getSetOfDamageTypes(JsonObject rootDict, String name) {
return getSetOfDamageTypes(rootDict, name, null);
}
@NonNull
public static Set<String> getSetOfDamageTypes(@NonNull 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;
}
@NonNull
public 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 = Helpers.getString(jsonObject, "name");
boolean canSpeak = Helpers.getBool(jsonObject, "speaks");
Language language = new Language(languageName, canSpeak);
languages.add(language);
}
}
}
}
return languages;
}
}
}

View File

@@ -0,0 +1,79 @@
package com.majinnaibu.monstercards.helpers;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collection;
@SuppressWarnings({"RedundantIfStatement"})
public final class StringHelper {
public static boolean isNullOrEmpty(CharSequence value) {
if (value == null) {
return true;
}
if ("".contentEquals(value)) {
return true;
}
return false;
}
@NonNull
public static String join(String delimiter, @NonNull Collection<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

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
package com.majinnaibu.monstercards.models;
import androidx.room.Entity;
import androidx.room.Fts4;
@Entity(tableName = "monsters_fts")
@Fts4(contentEntity = Monster.class)
public class MonsterFTS {
public String name;
public String size;
public String type;
public String subtype;
public String alignment;
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.monstercards.ui.collections;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
public class CollectionsFragment extends MCFragment {
private CollectionsViewModel collectionsViewModel;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
collectionsViewModel = new ViewModelProvider(this).get(CollectionsViewModel.class);
View root = inflater.inflate(R.layout.fragment_collections, container, false);
final TextView textView = root.findViewById(R.id.text_collections);
collectionsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
return root;
}
}

View File

@@ -0,0 +1,19 @@
package com.majinnaibu.monstercards.ui.collections;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class CollectionsViewModel extends ViewModel {
private final MutableLiveData<String> mText;
public CollectionsViewModel() {
mText = new MutableLiveData<>();
mText.setValue("This is collections fragment");
}
public LiveData<String> getText() {
return mText;
}
}

View File

@@ -0,0 +1,133 @@
package com.majinnaibu.monstercards.ui.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.helpers.ArrayHelper;
import java.util.Objects;
public class AbilityScorePicker extends LinearLayout {
private final ViewHolder mHolder;
private OnValueChangedListener mOnValueChangedListener;
private AbilityScore mSelectedValue;
private String mLabel;
public AbilityScorePicker(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mSelectedValue = AbilityScore.STRENGTH;
mOnValueChangedListener = null;
// TODO: use this as default but allow setting via attribute
mLabel = "Ability Score";
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AbilityScorePicker, 0, 0);
String label = a.getString(R.styleable.AbilityScorePicker_label);
if (label != null) {
mLabel = label;
}
a.recycle();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.component_ability_score_picker, this, true);
mHolder = new ViewHolder(root);
mHolder.label.setText(mLabel);
mHolder.spinner.setAdapter(new ArrayAdapter<AbilityScore>(getContext(), R.layout.dropdown_list_item, AbilityScore.values()) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
AbilityScore item = getItem(position);
TextView view = (TextView) super.getView(position, convertView, parent);
view.setText(item.displayName);
return view;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
AbilityScore item = getItem(position);
TextView view = (TextView) super.getDropDownView(position, convertView, parent);
view.setText(item.displayName);
return view;
}
});
mHolder.spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
setValue((AbilityScore) parent.getItemAtPosition(position));
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
setValue(mSelectedValue = AbilityScore.STRENGTH);
}
});
mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), mSelectedValue));
setValue(AbilityScore.STRENGTH);
}
public AbilityScorePicker(@NonNull Context context) {
this(context, null);
}
public AbilityScore getValue() {
return mSelectedValue;
}
public void setValue(AbilityScore value) {
if (value != mSelectedValue) {
mSelectedValue = value;
mHolder.spinner.setSelection(ArrayHelper.indexOf(AbilityScore.values(), value));
if (mOnValueChangedListener != null) {
mOnValueChangedListener.onValueChanged(value);
}
}
}
public void setOnValueChangedListener(OnValueChangedListener listener) {
mOnValueChangedListener = listener;
}
public String getLabel() {
return mLabel;
}
public void setLabel(String label) {
if (!Objects.equals(mLabel, label)) {
mLabel = label;
mHolder.label.setText(label);
}
}
public interface OnValueChangedListener {
void onValueChanged(AbilityScore value);
}
private static class ViewHolder {
private final Spinner spinner;
private final TextView label;
ViewHolder(@NonNull View root) {
spinner = root.findViewById(R.id.spinner);
label = root.findViewById(R.id.label);
}
}
}

View File

@@ -0,0 +1,98 @@
package com.majinnaibu.monstercards.ui.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RadioGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.google.android.material.radiobutton.MaterialRadioButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
@SuppressWarnings("unused")
public class AdvantagePicker extends ConstraintLayout {
private final ViewHolder mHolder;
private OnValueChangedListener mOnValueChangedListener;
private AdvantageType mSelectedValue;
public AdvantagePicker(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mSelectedValue = AdvantageType.NONE;
mOnValueChangedListener = null;
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.component_advantage_picker, this, true);
mHolder = new ViewHolder(root);
setValue(AdvantageType.NONE);
mHolder.group.setOnCheckedChangeListener((group, checkedId) -> {
if (R.id.hasAdvantage == checkedId) {
setValue(AdvantageType.ADVANTAGE);
} else if (R.id.hasDisadvantage == checkedId) {
setValue(AdvantageType.DISADVANTAGE);
} else {
setValue(AdvantageType.NONE);
}
});
}
public AdvantagePicker(@NonNull Context context) {
this(context, null);
}
public AdvantageType getValue() {
return mSelectedValue;
}
public void setValue(AdvantageType value) {
if (mSelectedValue != value) {
mSelectedValue = value;
if (mOnValueChangedListener != null) {
mOnValueChangedListener.onValueChanged(mSelectedValue);
}
}
final int checkedId = mHolder.group.getCheckedRadioButtonId();
if (mSelectedValue == AdvantageType.ADVANTAGE) {
if (checkedId != R.id.hasAdvantage) {
mHolder.advantage.setChecked(true);
}
} else if (mSelectedValue == AdvantageType.DISADVANTAGE) {
if (checkedId != R.id.hasDisadvantage) {
mHolder.disadvantage.setChecked(true);
}
} else {
if (checkedId != R.id.none) {
mHolder.none.setChecked(true);
}
}
}
public void setOnValueChangedListener(OnValueChangedListener listener) {
mOnValueChangedListener = listener;
}
public interface OnValueChangedListener {
void onValueChanged(AdvantageType value);
}
private static class ViewHolder {
final RadioGroup group;
final MaterialRadioButton none;
final MaterialRadioButton advantage;
final MaterialRadioButton disadvantage;
ViewHolder(@NonNull View root) {
group = root.findViewById(R.id.group);
none = root.findViewById(R.id.hasNoAdvantage);
advantage = root.findViewById(R.id.hasAdvantage);
disadvantage = root.findViewById(R.id.hasDisadvantage);
}
}
}

View File

@@ -0,0 +1,98 @@
package com.majinnaibu.monstercards.ui.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RadioGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.google.android.material.radiobutton.MaterialRadioButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
@SuppressWarnings("unused")
public class ProficiencyPicker extends ConstraintLayout {
private final ViewHolder mHolder;
private OnValueChangedListener mOnValueChangedListener;
private ProficiencyType mSelectedValue;
public ProficiencyPicker(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mSelectedValue = ProficiencyType.NONE;
mOnValueChangedListener = null;
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.component_proficiency_picker, this, true);
mHolder = new ViewHolder(root);
setValue(ProficiencyType.NONE);
mHolder.group.setOnCheckedChangeListener((group, checkedId) -> {
if (R.id.proficient == checkedId) {
setValue(ProficiencyType.PROFICIENT);
} else if (R.id.expertise == checkedId) {
setValue(ProficiencyType.EXPERTISE);
} else {
setValue(ProficiencyType.NONE);
}
});
}
public ProficiencyPicker(@NonNull Context context) {
this(context, null);
}
public ProficiencyType getValue() {
return mSelectedValue;
}
public void setValue(ProficiencyType value) {
if (mSelectedValue != value) {
mSelectedValue = value;
if (mOnValueChangedListener != null) {
mOnValueChangedListener.onValueChanged(mSelectedValue);
}
}
final int checkedId = mHolder.group.getCheckedRadioButtonId();
if (mSelectedValue == ProficiencyType.PROFICIENT) {
if (checkedId != R.id.proficient) {
mHolder.proficient.setChecked(true);
}
} else if (mSelectedValue == ProficiencyType.EXPERTISE) {
if (checkedId != R.id.expertise) {
mHolder.expertise.setChecked(true);
}
} else {
if (checkedId != R.id.none) {
mHolder.none.setChecked(true);
}
}
}
public void setOnValueChangedListener(OnValueChangedListener listener) {
mOnValueChangedListener = listener;
}
public interface OnValueChangedListener {
void onValueChanged(ProficiencyType value);
}
private static class ViewHolder {
final RadioGroup group;
final MaterialRadioButton none;
final MaterialRadioButton proficient;
final MaterialRadioButton expertise;
ViewHolder(@NonNull View root) {
group = root.findViewById(R.id.group);
none = root.findViewById(R.id.none);
proficient = root.findViewById(R.id.proficient);
expertise = root.findViewById(R.id.expertise);
}
}
}

View File

@@ -0,0 +1,149 @@
package com.majinnaibu.monstercards.ui.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.majinnaibu.monstercards.R;
import java.util.Objects;
@SuppressWarnings("unused")
public class Stepper extends ConstraintLayout {
private final ViewHolder mHolder;
private int mCurrentValue;
private int mStep;
private int mMinValue;
private int mMaxValue;
private String mLabel;
private OnValueChangeListener mOnValueChangeListener;
private OnFormatValueCallback mOnFormatValueCallback;
public Stepper(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mCurrentValue = 0;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Stepper, 0, 0);
mStep = a.getInt(R.styleable.Stepper_stepAmount, 1);
mMinValue = a.getInt(R.styleable.Stepper_minValue, Integer.MIN_VALUE);
mMaxValue = a.getInt(R.styleable.Stepper_maxValue, Integer.MAX_VALUE);
mLabel = a.getString(R.styleable.Stepper_label);
a.recycle();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View root = inflater.inflate(R.layout.component_stepper, this, true);
mHolder = new ViewHolder(root);
setValue(mCurrentValue);
updateDisplayedValue();
mHolder.increment.setOnClickListener(v -> setValue(mCurrentValue + mStep));
mHolder.decrement.setOnClickListener(v -> setValue(mCurrentValue - mStep));
mHolder.label.setText(mLabel);
}
public Stepper(Context context) {
this(context, null);
}
public String getLabel() {
return mLabel;
}
public void setLabel(String newLabel) {
if (!Objects.equals(mLabel, newLabel)) {
mLabel = newLabel;
mHolder.label.setText(mLabel);
}
}
public int getValue() {
return mCurrentValue;
}
public void setValue(int value) {
int oldValue = this.mCurrentValue;
int newValue = Math.min(mMaxValue, Math.max(mMinValue, value));
if (newValue != oldValue) {
this.mCurrentValue = newValue;
if (mOnValueChangeListener != null) {
mOnValueChangeListener.onChange(newValue, oldValue);
}
updateDisplayedValue();
}
}
private void updateDisplayedValue() {
if (mOnFormatValueCallback != null) {
mHolder.text.setText(mOnFormatValueCallback.onFormatValue(this.mCurrentValue));
} else {
mHolder.text.setText(String.valueOf(this.mCurrentValue));
}
}
public void setOnValueChangeListener(OnValueChangeListener listener) {
mOnValueChangeListener = listener;
}
public void setOnFormatValueCallback(OnFormatValueCallback callback) {
mOnFormatValueCallback = callback;
updateDisplayedValue();
}
public int getStep() {
return mStep;
}
public void setStep(int step) {
this.mStep = step;
}
public int getMinValue() {
return mMinValue;
}
public void setMinValue(int minValue) {
this.mMinValue = minValue;
}
public int getMaxValue() {
return mMaxValue;
}
public void setMaxValue(int maxValue) {
this.mMaxValue = maxValue;
}
public interface OnValueChangeListener {
void onChange(int value, int previousValue);
}
public interface OnFormatValueCallback {
String onFormatValue(int value);
}
private static class ViewHolder {
final TextView text;
final TextView label;
final Button increment;
final Button decrement;
ViewHolder(@NonNull View root) {
text = root.findViewById(R.id.text);
label = root.findViewById(R.id.label);
increment = root.findViewById(R.id.increment);
decrement = root.findViewById(R.id.decrement);
}
}
}

View File

@@ -0,0 +1,83 @@
package com.majinnaibu.monstercards.ui.dashboard;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class DashboardFragment extends MCFragment {
private DashboardViewModel mViewModel;
private ViewHolder mHolder;
private DashboardRecyclerViewAdapter mAdapter;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this).get(DashboardViewModel.class);
View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
mHolder = new ViewHolder(root);
setupRecyclerView(mHolder.list);
// TODO: subscribe better
getMonsterRepository()
.getMonsters()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> mViewModel.setMonsters(monsters));
return root;
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 396);
Context context = requireContext();
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<Monster>> monsterData = mViewModel.getMonsters();
mAdapter = new DashboardRecyclerViewAdapter(monster -> {
if (monster != null) {
navigateToMonsterDetail(monster);
} else {
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
}
});
if (monsterData != null) {
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
}
recyclerView.setAdapter(mAdapter);
}
private void navigateToMonsterDetail(Monster monster) {
NavDirections action = DashboardFragmentDirections.actionNavigationDashboardToNavigationMonster(monster.id.toString());
Navigation.findNavController(requireView()).navigate(action);
}
private static class ViewHolder {
final RecyclerView list;
ViewHolder(View root) {
list = root.findViewById(R.id.list);
}
}
}

View File

@@ -0,0 +1,353 @@
package com.majinnaibu.monstercards.ui.dashboard;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.databinding.CardMonsterBinding;
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.Trait;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.Locale;
public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, DashboardRecyclerViewAdapter.ViewHolder> {
private static final DiffUtil.ItemCallback<Monster> DIFF_CALLBACK = new DiffUtil.ItemCallback<Monster>() {
@Override
public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return oldItem.id.equals(newItem.id);
}
@Override
public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
return oldItem.equals(newItem);
}
};
private final ItemCallback mOnClick;
protected DashboardRecyclerViewAdapter(ItemCallback onClick) {
super(DIFF_CALLBACK);
mOnClick = onClick;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(CardMonsterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Logger.logUnimplementedMethod();
Monster monster = getItem(position);
holder.monster = monster;
holder.name.setText(monster.name);
holder.meta.setText(monster.getMeta());
holder.strengthAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.strengthSavingThrowAdvantage));
holder.strengthModifier.setText(Helpers.getModifierString(monster.getStrengthModifier()));
holder.strengthName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.STRENGTH));
holder.strengthProficiency.setText(Helpers.getProficiencyAbbreviation(monster.strengthSavingThrowProficiency));
holder.dexterityAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.dexteritySavingThrowAdvantage));
holder.dexterityModifier.setText(Helpers.getModifierString(monster.getDexterityModifier()));
holder.dexterityName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.DEXTERITY));
holder.dexterityProficiency.setText(Helpers.getProficiencyAbbreviation(monster.dexteritySavingThrowProficiency));
holder.constitutionAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.constitutionSavingThrowAdvantage));
holder.constitutionModifier.setText(Helpers.getModifierString(monster.getConstitutionModifier()));
holder.constitutionName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.CONSTITUTION));
holder.constitutionProficiency.setText(Helpers.getProficiencyAbbreviation(monster.constitutionSavingThrowProficiency));
holder.intelligenceAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.intelligenceSavingThrowAdvantage));
holder.intelligenceModifier.setText(Helpers.getModifierString(monster.getIntelligenceModifier()));
holder.intelligenceName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.INTELLIGENCE));
holder.intelligenceProficiency.setText(Helpers.getProficiencyAbbreviation(monster.intelligenceSavingThrowProficiency));
holder.wisdomAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.wisdomSavingThrowAdvantage));
holder.wisdomModifier.setText(Helpers.getModifierString(monster.getWisdomModifier()));
holder.wisdomName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.WISDOM));
holder.wisdomProficiency.setText(Helpers.getProficiencyAbbreviation(monster.wisdomSavingThrowProficiency));
holder.charismaAdvantage.setText(Helpers.getAdvantageAbbreviation(monster.charismaSavingThrowAdvantage));
holder.charismaModifier.setText(Helpers.getModifierString(monster.getCharismaModifier()));
holder.charismaName.setText(Helpers.getAbilityScoreAbbreviation(AbilityScore.CHARISMA));
holder.charismaProficiency.setText(Helpers.getProficiencyAbbreviation(monster.charismaSavingThrowProficiency));
holder.armorClass.setText(String.valueOf(monster.getArmorClassValue()));
holder.hitPoints.setText(String.valueOf(monster.getHitPointsValue()));
holder.challengeRating.setText(holder.challengeRating.getResources().getString(R.string.label_challenge_rating_with_value, Helpers.getChallengeRatingAbbreviation(monster.challengeRating)));
int numActions = monster.actions.size();
if (numActions > 0) {
holder.action1Group.setVisibility(View.VISIBLE);
Trait action = monster.actions.get(0);
holder.action1Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description)));
holder.action1Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name)));
} else {
holder.action1Group.setVisibility(View.GONE);
}
if (numActions > 1) {
holder.action2Group.setVisibility(View.VISIBLE);
Trait action = monster.actions.get(1);
holder.action2Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description)));
holder.action2Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name)));
} else {
holder.action2Group.setVisibility(View.GONE);
}
if (numActions > 2) {
holder.action3Group.setVisibility(View.VISIBLE);
Trait action = monster.actions.get(2);
holder.action3Description.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.description)));
holder.action3Name.setText(Html.fromHtml(CommonMarkHelper.toHtml(action.name)));
} else {
holder.action3Group.setVisibility(View.GONE);
}
holder.itemView.setOnClickListener(v -> {
if (mOnClick != null) {
mOnClick.onItemCallback(holder.monster);
}
});
}
public interface ItemCallback {
void onItemCallback(Monster monster);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView name;
public final TextView meta;
public final View action1Group;
public final TextView action1Name;
public final TextView action1Description;
public final View action2Group;
public final TextView action2Name;
public final TextView action2Description;
public final View action3Group;
public final TextView action3Name;
public final TextView action3Description;
public final TextView strengthName;
public final TextView strengthModifier;
public final TextView strengthProficiency;
public final TextView strengthAdvantage;
public final TextView dexterityName;
public final TextView dexterityModifier;
public final TextView dexterityProficiency;
public final TextView dexterityAdvantage;
public final TextView constitutionName;
public final TextView constitutionModifier;
public final TextView constitutionProficiency;
public final TextView constitutionAdvantage;
public final TextView intelligenceName;
public final TextView intelligenceModifier;
public final TextView intelligenceProficiency;
public final TextView intelligenceAdvantage;
public final TextView wisdomName;
public final TextView wisdomModifier;
public final TextView wisdomProficiency;
public final TextView wisdomAdvantage;
public final TextView charismaName;
public final TextView charismaModifier;
public final TextView charismaProficiency;
public final TextView charismaAdvantage;
public final TextView armorClass;
public final TextView hitPoints;
public final TextView challengeRating;
public Monster monster;
public ViewHolder(@NonNull CardMonsterBinding binding) {
super(binding.getRoot());
name = binding.name;
meta = binding.meta;
action1Group = binding.action1.getRoot();
action1Name = binding.action1.name;
action1Description = binding.action1.description;
action2Group = binding.action2.getRoot();
action2Name = binding.action2.name;
action2Description = binding.action2.description;
action3Group = binding.action3.getRoot();
action3Name = binding.action3.name;
action3Description = binding.action3.description;
strengthName = binding.strength.name;
strengthModifier = binding.strength.modifier;
strengthProficiency = binding.strength.proficiency;
strengthAdvantage = binding.strength.advantage;
dexterityName = binding.dexterity.name;
dexterityModifier = binding.dexterity.modifier;
dexterityProficiency = binding.dexterity.proficiency;
dexterityAdvantage = binding.dexterity.advantage;
constitutionName = binding.constitution.name;
constitutionModifier = binding.constitution.modifier;
constitutionProficiency = binding.constitution.proficiency;
constitutionAdvantage = binding.constitution.advantage;
intelligenceName = binding.intelligence.name;
intelligenceModifier = binding.intelligence.modifier;
intelligenceProficiency = binding.intelligence.proficiency;
intelligenceAdvantage = binding.intelligence.advantage;
wisdomName = binding.wisdom.name;
wisdomModifier = binding.wisdom.modifier;
wisdomProficiency = binding.wisdom.proficiency;
wisdomAdvantage = binding.wisdom.advantage;
charismaName = binding.charisma.name;
charismaModifier = binding.charisma.modifier;
charismaProficiency = binding.charisma.proficiency;
charismaAdvantage = binding.charisma.advantage;
armorClass = binding.armorClass.value;
hitPoints = binding.hitPoints.value;
challengeRating = binding.challengeRating;
}
}
public static class Helpers {
@NonNull
public static String getModifierString(int value) {
return String.format(Locale.getDefault(), "%+d", value);
}
@NonNull
public static String getAbilityScoreAbbreviation(@NonNull AbilityScore abilityScore) {
switch (abilityScore) {
case STRENGTH:
return "S";
case DEXTERITY:
return "D";
case CONSTITUTION:
return "C";
case INTELLIGENCE:
return "I";
case WISDOM:
return "W";
case CHARISMA:
return "Ch";
default:
Logger.logUnimplementedFeature(String.format("Get an abbreviation for AbilityScore value %s", abilityScore));
return "";
}
}
@NonNull
public static String getChallengeRatingAbbreviation(@NonNull ChallengeRating challengeRating) {
Logger.logUnimplementedMethod();
switch (challengeRating) {
case CUSTOM:
return "*";
case ZERO:
return "0";
case ONE_EIGHTH:
return "1/8";
case ONE_QUARTER:
return "1/4";
case ONE_HALF:
return "1/2";
case ONE:
return "1";
case TWO:
return "2";
case THREE:
return "3";
case FOUR:
return "4";
case FIVE:
return "5";
case SIX:
return "6";
case SEVEN:
return "7";
case EIGHT:
return "8";
case NINE:
return "9";
case TEN:
return "10";
case ELEVEN:
return "11";
case TWELVE:
return "12";
case THIRTEEN:
return "13";
case FOURTEEN:
return "14";
case FIFTEEN:
return "15";
case SIXTEEN:
return "16";
case SEVENTEEN:
return "17";
case EIGHTEEN:
return "18";
case NINETEEN:
return "19";
case TWENTY:
return "20";
case TWENTY_ONE:
return "21";
case TWENTY_TWO:
return "22";
case TWENTY_THREE:
return "23";
case TWENTY_FOUR:
return "24";
case TWENTY_FIVE:
return "25";
case TWENTY_SIX:
return "26";
case TWENTY_SEVEN:
return "27";
case TWENTY_EIGHT:
return "28";
case TWENTY_NINE:
return "29";
case THIRTY:
return "30";
default:
Logger.logUnimplementedFeature(String.format("Get an abbreviation for ChallengeRating value %s", challengeRating));
return "";
}
}
@NonNull
public static String getProficiencyAbbreviation(@NonNull ProficiencyType proficiency) {
switch (proficiency) {
case NONE:
return "";
case EXPERTISE:
return "E";
case PROFICIENT:
return "P";
default:
Logger.logUnimplementedFeature(String.format("Get an abbreviation for ProficiencyType value %s", proficiency));
return "";
}
}
@NonNull
public static String getAdvantageAbbreviation(@NonNull AdvantageType advantage) {
switch (advantage) {
case NONE:
return "";
case ADVANTAGE:
return "A";
case DISADVANTAGE:
return "D";
default:
Logger.logUnimplementedFeature(String.format("Get an abbreviation for AdvantageType value %s", advantage));
return "";
}
}
}
}

View File

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

View File

@@ -0,0 +1,82 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.ui.components.Stepper;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import java.util.Locale;
public class EditAbilityScoresFragment extends MCFragment {
private final String ABILITY_SCORE_FORMAT = "%d (%+d)";
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
private int getModifier(int value) {
return value / 2 - 5;
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_ability_scores, container, false);
mHolder = new ViewHolder(root);
mViewModel.getStrength().observe(getViewLifecycleOwner(), value -> mHolder.strength.setValue(value));
mHolder.strength.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setStrength(newValue));
mHolder.strength.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
mViewModel.getDexterity().observe(getViewLifecycleOwner(), value -> mHolder.dexterity.setValue(value));
mHolder.dexterity.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setDexterity(newValue));
mHolder.dexterity.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
mViewModel.getConstitution().observe(getViewLifecycleOwner(), value -> mHolder.constitution.setValue(value));
mHolder.constitution.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setConstitution(newValue));
mHolder.constitution.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
mViewModel.getIntelligence().observe(getViewLifecycleOwner(), value -> mHolder.intelligence.setValue(value));
mHolder.intelligence.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setIntelligence(newValue));
mHolder.intelligence.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
mViewModel.getWisdom().observe(getViewLifecycleOwner(), value -> mHolder.wisdom.setValue(value));
mHolder.wisdom.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setWisdom(newValue));
mHolder.wisdom.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
mViewModel.getCharisma().observe(getViewLifecycleOwner(), value -> mHolder.charisma.setValue(value));
mHolder.charisma.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setCharisma(newValue));
mHolder.charisma.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), ABILITY_SCORE_FORMAT, value, getModifier(value)));
return root;
}
private static class ViewHolder {
final Stepper strength;
final Stepper dexterity;
final Stepper constitution;
final Stepper intelligence;
final Stepper wisdom;
final Stepper charisma;
ViewHolder(@NonNull View root) {
strength = root.findViewById(R.id.strength);
dexterity = root.findViewById(R.id.dexterity);
constitution = root.findViewById(R.id.constitution);
intelligence = root.findViewById(R.id.intelligence);
wisdom = root.findViewById(R.id.wisdom);
charisma = root.findViewById(R.id.charisma);
}
}
}

View File

@@ -0,0 +1,104 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.ArmorType;
import com.majinnaibu.monstercards.helpers.ArrayHelper;
import com.majinnaibu.monstercards.ui.components.Stepper;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditArmorFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_armor, container, false);
mHolder = new ViewHolder(root);
mHolder.armorType.setAdapter(new ArrayAdapter<ArmorType>(requireContext(), R.layout.dropdown_list_item, ArmorType.values()) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ArmorType item = getItem(position);
TextView view = (TextView) super.getView(position, convertView, parent);
view.setText(item.displayName);
return view;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ArmorType item = getItem(position);
TextView view = (TextView) super.getDropDownView(position, convertView, parent);
view.setText(item.displayName);
return view;
}
});
mHolder.armorType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
ArmorType selectedItem = (ArmorType) parent.getItemAtPosition(position);
mViewModel.setArmorType(selectedItem);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
mViewModel.setArmorType(ArmorType.NONE);
}
});
mHolder.armorType.setSelection(ArrayHelper.indexOf(ArmorType.values(), mViewModel.getArmorType().getValue()));
mHolder.naturalArmorBonus.setValue(mViewModel.getNaturalArmorBonusUnboxed());
mHolder.naturalArmorBonus.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setNaturalArmorBonus(newValue));
mHolder.hasShield.setChecked(mViewModel.getHasShieldValueAsBoolean());
mHolder.hasShield.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setHasShield(isChecked));
mHolder.shieldBonus.setValue(mViewModel.getShieldBonusUnboxed());
mHolder.shieldBonus.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setShieldBonus(newValue));
mHolder.customArmor.setText(mViewModel.getCustomArmor().getValue());
mHolder.customArmor.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomArmor(s.toString()))));
return root;
}
private static class ViewHolder {
private final Spinner armorType;
private final Stepper naturalArmorBonus;
private final SwitchCompat hasShield;
private final Stepper shieldBonus;
private final EditText customArmor;
ViewHolder(@NonNull View root) {
armorType = root.findViewById(R.id.armorType);
naturalArmorBonus = root.findViewById(R.id.naturalArmorBonus);
hasShield = root.findViewById(R.id.hasShield);
shieldBonus = root.findViewById(R.id.shieldBonus);
customArmor = root.findViewById(R.id.customArmor);
}
}
}

View File

@@ -0,0 +1,88 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.ui.components.Stepper;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditBasicInfoFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
@Override
public void onStart() {
super.onStart();
mHolder.name.requestFocus();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_basic_info, container, false);
mHolder = new ViewHolder(root);
mHolder.name.setText(mViewModel.getName().getValue());
mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString())));
mHolder.size.setText(mViewModel.getSize().getValue());
mHolder.size.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setSize(s.toString())));
mHolder.type.setText(mViewModel.getType().getValue());
mHolder.type.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setType(s.toString())));
mHolder.subtype.setText(mViewModel.getSubtype().getValue());
mHolder.subtype.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setSubtype(s.toString())));
mHolder.alignment.setText(mViewModel.getAlignment().getValue());
mHolder.alignment.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setAlignment(s.toString())));
mHolder.customHitPoints.setText(mViewModel.getCustomHitPoints().getValue());
mHolder.customHitPoints.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomHitPoints(s.toString()))));
mHolder.hitDice.setValue(mViewModel.getHitDiceUnboxed());
mHolder.hitDice.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setHitDice(newValue));
mHolder.hasCustomHitPoints.setChecked(mViewModel.getHasCustomHitPointsValueAsBoolean());
mHolder.hasCustomHitPoints.setOnCheckedChangeListener((button, isChecked) -> mViewModel.setHasCustomHitPoints(isChecked));
return root;
}
private static class ViewHolder {
private final EditText name;
private final EditText size;
private final EditText type;
private final EditText subtype;
private final EditText alignment;
private final EditText customHitPoints;
private final Stepper hitDice;
private final SwitchMaterial hasCustomHitPoints;
ViewHolder(@NonNull View root) {
name = root.findViewById(R.id.name);
size = root.findViewById(R.id.size);
type = root.findViewById(R.id.type);
subtype = root.findViewById(R.id.subtype);
alignment = root.findViewById(R.id.alignment);
customHitPoints = root.findViewById(R.id.customHitPoints);
hitDice = root.findViewById(R.id.hitDice);
hasCustomHitPoints = root.findViewById(R.id.hasCustomHitPoints);
}
}
}

View File

@@ -0,0 +1,91 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
import com.majinnaibu.monstercards.helpers.ArrayHelper;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditChallengeRatingFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_challenge_rating, container, false);
mHolder = new ViewHolder(root);
mHolder.challengeRating.setAdapter(new ArrayAdapter<ChallengeRating>(requireContext(), R.layout.dropdown_list_item, ChallengeRating.values()) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ChallengeRating item = getItem(position);
TextView view = (TextView) super.getView(position, convertView, parent);
view.setText(item.displayName);
return view;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ChallengeRating item = getItem(position);
TextView view = (TextView) super.getDropDownView(position, convertView, parent);
view.setText(item.displayName);
return view;
}
});
mHolder.challengeRating.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
ChallengeRating selectedItem = (ChallengeRating) parent.getItemAtPosition(position);
mViewModel.setChallengeRating(selectedItem);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
mViewModel.setChallengeRating(ChallengeRating.CUSTOM);
}
});
mHolder.challengeRating.setSelection(ArrayHelper.indexOf(ChallengeRating.values(), mViewModel.getChallengeRating().getValue()));
mHolder.customChallengeRatingDescription.setText(mViewModel.getCustomChallengeRatingDescription().getValue());
mHolder.customChallengeRatingDescription.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomChallengeRatingDescription(s.toString()))));
mHolder.customProficiencyBonus.setText(mViewModel.getCustomProficiencyBonusValueAsString());
mHolder.customProficiencyBonus.addTextChangedListener((new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomProficiencyBonus(s.toString()))));
return root;
}
private static class ViewHolder {
final Spinner challengeRating;
final EditText customChallengeRatingDescription;
final EditText customProficiencyBonus;
ViewHolder(@NonNull View root) {
challengeRating = root.findViewById(R.id.challengeRating);
customChallengeRatingDescription = root.findViewById(R.id.customChallengeRatingDescription);
customProficiencyBonus = root.findViewById(R.id.customProficiencyBonus);
}
}
}

View File

@@ -0,0 +1,88 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditLanguageFragment extends MCFragment {
private EditMonsterViewModel mEditMonsterViewModel;
private EditLanguageViewModel mViewModel;
private ViewHolder mHolder;
private Language mOldLanguage;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this).get(EditLanguageViewModel.class);
Bundle arguments = getArguments();
if (arguments != null) {
EditLanguageFragmentArgs args = EditLanguageFragmentArgs.fromBundle(arguments);
mOldLanguage = new Language(args.getName(), args.getCanSpeak());
mViewModel.copyFromLanguage(mOldLanguage);
} else {
Logger.logWTF("EditLanguageFragment needs arguments");
mOldLanguage = null;
}
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_language, container, false);
mHolder = new ViewHolder(root);
mHolder.name.setText(mViewModel.getName().getValue());
mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString())));
mHolder.canSpeak.setChecked(mViewModel.getCanSpeakValue());
mHolder.canSpeak.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setCanSpeak(isChecked));
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (mViewModel.hasChanges()) {
mEditMonsterViewModel.replaceLanguage(mOldLanguage, mViewModel.getLanguage().getValue());
}
Navigation.findNavController(requireView()).navigateUp();
}
});
return root;
}
@Override
public void onStart() {
super.onStart();
mHolder.name.requestFocus();
}
private static class ViewHolder {
EditText name;
SwitchCompat canSpeak;
ViewHolder(@NonNull View root) {
name = root.findViewById(R.id.name);
canSpeak = root.findViewById(R.id.canSpeak);
}
}
}

View File

@@ -0,0 +1,68 @@
package com.majinnaibu.monstercards.ui.editmonster;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel;
import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData;
public class EditLanguageViewModel extends ChangeTrackedViewModel {
private final ChangeTrackedLiveData<String> mName;
private final ChangeTrackedLiveData<Boolean> mCanSpeak;
private final ChangeTrackedLiveData<Language> mLanguage;
public EditLanguageViewModel() {
super();
mName = new ChangeTrackedLiveData<>("New Language", this::makeDirty);
mCanSpeak = new ChangeTrackedLiveData<>(true, this::makeDirty);
mLanguage = new ChangeTrackedLiveData<>(makeLanguage(), this::makeDirty);
}
public void copyFromLanguage(@NonNull Language language) {
mName.resetValue(language.getName());
mCanSpeak.resetValue(language.getSpeaks());
makeClean();
}
public LiveData<Language> getLanguage() {
return mLanguage;
}
public LiveData<String> getName() {
return mName;
}
public void setName(String name) {
mName.setValue(name);
mLanguage.setValue(makeLanguage());
}
public LiveData<Boolean> getCanSpeak() {
return mCanSpeak;
}
public void setCanSpeak(boolean canSpeak) {
mCanSpeak.setValue(canSpeak);
mLanguage.setValue(makeLanguage());
}
public boolean getCanSpeakValue(boolean defaultIfNull) {
Boolean boxedValue = mCanSpeak.getValue();
if (boxedValue == null) {
return defaultIfNull;
}
return boxedValue;
}
public boolean getCanSpeakValue() {
return getCanSpeakValue(false);
}
@NonNull
private Language makeLanguage() {
Boolean boxedValue = mCanSpeak.getValue();
boolean canSpeak = boxedValue != null && boxedValue;
return new Language(mName.getValue(), canSpeak);
}
}

View File

@@ -0,0 +1,101 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditLanguagesFragment extends MCFragment {
// TODO: Make the swipe to delete not happen for the header
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
private void navigateToEditLanguage(@NonNull Language language) {
NavDirections action = EditLanguagesFragmentDirections.actionEditLanguagesFragmentToEditLanguageFragment(language.getName(), language.getSpeaks());
Navigation.findNavController(requireView()).navigate(action);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_languages_list, container, false);
mHolder = new ViewHolder(root);
setupRecyclerView(mHolder.list);
setupAddLanguageButton(mHolder.addLanguage);
return root;
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> {
EditLanguagesRecyclerViewAdapter adapter = new EditLanguagesRecyclerViewAdapter(
mViewModel.getLanguagesArray(),
language -> {
if (language != null) {
navigateToEditLanguage(language);
} else {
Logger.logError("Can't navigate to EditSkill with a null skill");
}
},
mViewModel.getTelepathyRangeUnboxed(),
(value, previousValue) -> mViewModel.setTelepathyRange(value),
mViewModel.getUnderstandsButDescription().getValue(),
new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setUnderstandsButDescription(s.toString())));
recyclerView.setAdapter(adapter);
});
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> {
if (position > 0) {
mViewModel.removeLanguage(position - 1);
}
}, null));
itemTouchHelper.attachToRecyclerView(recyclerView);
}
private void setupAddLanguageButton(@NonNull FloatingActionButton fab) {
fab.setOnClickListener(view -> {
Language newLanguage = mViewModel.addNewLanguage();
navigateToEditLanguage(newLanguage);
});
}
private static class ViewHolder {
RecyclerView list;
FloatingActionButton addLanguage;
ViewHolder(@NonNull View root) {
this.list = root.findViewById(R.id.list);
this.addLanguage = root.findViewById(R.id.add_language);
}
}
}

View File

@@ -0,0 +1,114 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListHeaderBinding;
import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListItemBinding;
import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.ui.components.Stepper;
import java.util.List;
import java.util.Locale;
public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<Language> mValues;
private final ItemCallback mOnClick;
private final int mTelepathyRange;
private final String mUnderstandsBut;
private final Stepper.OnValueChangeListener mOnTelepathyRangeChanged;
private final TextWatcher mOnUnderstandsButChanged;
private final int HEADER_VIEW_TYPE = 1;
private final int ITEM_VIEW_TYPE = 2;
private final String DISTANCE_IN_FEET_FORMAT = "%d ft.";
public EditLanguagesRecyclerViewAdapter(List<Language> items, ItemCallback onClick, int telepathyRange, Stepper.OnValueChangeListener telepathyRangeChangedListener, String understandsBut, TextWatcher understandsButChangedListener) {
mValues = items;
mOnClick = onClick;
mTelepathyRange = telepathyRange;
mOnTelepathyRangeChanged = telepathyRangeChangedListener;
mUnderstandsBut = understandsBut;
mOnUnderstandsButChanged = understandsButChangedListener;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == HEADER_VIEW_TYPE) {
return new HeaderViewHolder(FragmentEditLanguagesListHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
return new ItemViewHolder(FragmentEditLanguagesListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HeaderViewHolder) {
HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder;
headerViewHolder.telepathy.setOnFormatValueCallback(value -> String.format(Locale.getDefault(), DISTANCE_IN_FEET_FORMAT, value));
headerViewHolder.telepathy.setValue(mTelepathyRange);
headerViewHolder.telepathy.setOnValueChangeListener(mOnTelepathyRangeChanged);
headerViewHolder.understandsBut.setText(mUnderstandsBut);
headerViewHolder.understandsBut.addTextChangedListener(mOnUnderstandsButChanged);
} else if (holder instanceof ItemViewHolder) {
ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
itemViewHolder.mItem = mValues.get(position - 1);
itemViewHolder.mContentView.setText(itemViewHolder.mItem.getName());
itemViewHolder.itemView.setOnClickListener(view -> {
if (mOnClick != null) {
mOnClick.onItemCallback(itemViewHolder.mItem);
}
});
}
}
@Override
public int getItemCount() {
return mValues.size() + 1;
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return HEADER_VIEW_TYPE;
}
return ITEM_VIEW_TYPE;
}
public interface ItemCallback {
void onItemCallback(Language language);
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
public final Stepper telepathy;
public final EditText understandsBut;
public HeaderViewHolder(@NonNull FragmentEditLanguagesListHeaderBinding binding) {
super(binding.getRoot());
telepathy = binding.telepathy;
understandsBut = binding.understandsBut;
}
}
public static class ItemViewHolder extends RecyclerView.ViewHolder {
public final TextView mContentView;
public Language mItem;
public ItemViewHolder(@NonNull FragmentEditLanguagesListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -0,0 +1,272 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.data.enums.StringType;
import com.majinnaibu.monstercards.data.enums.TraitType;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentArgs;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.Objects;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
import io.reactivex.rxjava3.observers.DisposableSingleObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class EditMonsterFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
MonsterRepository repository = getMonsterRepository();
Bundle arguments = getArguments();
assert arguments != null;
UUID monsterId = UUID.fromString(MonsterDetailFragmentArgs.fromBundle(arguments).getMonsterId());
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_monster, container, false);
mHolder = new ViewHolder(root);
setTitle(getString(R.string.title_editMonster_fmt, getString(R.string.default_monster_name)));
// TODO: Show a loading spinner until we have the monster loaded.
if (mViewModel.hasError() || !mViewModel.hasLoaded() || !Objects.equals(mViewModel.getMonsterId().getValue(), monsterId)) {
repository.getMonster(monsterId).toObservable()
.firstOrError()
.subscribe(new DisposableSingleObserver<Monster>() {
@Override
public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) {
mViewModel.setHasLoaded(true);
mViewModel.setHasError(false);
mViewModel.copyFromMonster(monster);
setTitle(getString(R.string.title_editMonster_fmt, monster.name));
dispose();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
// TODO: Show an error state.
Logger.logError(e);
mViewModel.setHasError(true);
mViewModel.setErrorMessage(e.toString());
dispose();
}
});
}
mHolder.basicInfoButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.armorButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditArmorFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.speedButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSpeedFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.abilityScoresButton.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditAbilityScoresFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.savingThrows.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSavingThrowsFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.challengeRating.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditChallengeRatingFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.skills.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSkillsFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.senses.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.SENSE);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.conditionImmunities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.CONDITION_IMMUNITY);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.damageImmunities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_IMMUNITY);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.damageResistances.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_RESISTANCE);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.damageVulnerabilities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_VULNERABILITY);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.languages.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditLanguagesFragment();
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.abilities.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ABILITY);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.actions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ACTION);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.lairActions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LAIR_ACTION);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.legendaryActions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LEGENDARY_ACTION);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.reactions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REACTIONS);
Navigation.findNavController(requireView()).navigate(action);
});
mHolder.regionalActions.setOnClickListener(v -> {
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REGIONAL_ACTION);
Navigation.findNavController(requireView()).navigate(action);
});
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (mViewModel.hasChanges()) {
View view = getView();
AlertDialog alertDialog = new AlertDialog.Builder(requireContext()).create();
alertDialog.setTitle("Unsaved Changes");
alertDialog.setMessage("Do you want to save your changes?");
alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Save", (dialog, id) -> {
// Save the monster. Navigate up if the save is successful. Show a SnackBar if there was an error.
getMonsterRepository().saveMonster(mViewModel.buildMonster())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
new DisposableCompletableObserver() {
@Override
public void onComplete() {
Navigation.findNavController(requireView()).navigateUp();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error creating monster", e);
assert view != null;
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
});
alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Discard", (dialog, id) -> {
// Navigate up ignoring unsaved changes.
Navigation.findNavController(requireView()).navigateUp();
});
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "Cancel", (dialog, id) -> {
// Do nothing.
});
alertDialog.show();
} else {
// No changes so we can safely leave.
Navigation.findNavController(requireView()).navigateUp();
}
}
});
return root;
}
private static class ViewHolder {
TextView basicInfoButton;
TextView armorButton;
TextView speedButton;
TextView abilityScoresButton;
TextView savingThrows;
TextView skills;
TextView conditionImmunities;
TextView damageImmunities;
TextView damageResistances;
TextView damageVulnerabilities;
TextView senses;
TextView languages;
TextView challengeRating;
TextView abilities;
TextView actions;
TextView reactions;
TextView legendaryActions;
TextView lairActions;
TextView regionalActions;
ViewHolder(@NonNull View root) {
basicInfoButton = root.findViewById(R.id.basicInfo);
armorButton = root.findViewById(R.id.armor);
speedButton = root.findViewById(R.id.speed);
abilityScoresButton = root.findViewById(R.id.abilityScores);
savingThrows = root.findViewById(R.id.savingThrows);
skills = root.findViewById(R.id.skills);
conditionImmunities = root.findViewById(R.id.conditionImmunities);
damageImmunities = root.findViewById(R.id.damageImmunities);
damageResistances = root.findViewById(R.id.damageResistances);
damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities);
senses = root.findViewById(R.id.senses);
languages = root.findViewById(R.id.languages);
challengeRating = root.findViewById(R.id.challengeRating);
abilities = root.findViewById(R.id.abilities);
actions = root.findViewById(R.id.actions);
reactions = root.findViewById(R.id.reactions);
legendaryActions = root.findViewById(R.id.legendaryActions);
lairActions = root.findViewById(R.id.lairActions);
regionalActions = root.findViewById(R.id.regionalActions);
}
}
}

View File

@@ -0,0 +1,94 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.ui.components.AdvantagePicker;
import com.majinnaibu.monstercards.ui.components.ProficiencyPicker;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
public class EditSavingThrowsFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mViewHolder;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_saving_throws, container, false);
mViewHolder = new ViewHolder(root);
mViewHolder.strengthProficiency.setValue(mViewModel.getStrengthProficiency().getValue());
mViewHolder.strengthProficiency.setOnValueChangedListener(value -> mViewModel.setStrengthProficiency(value));
mViewHolder.strengthAdvantage.setValue(mViewModel.getStrengthAdvantage().getValue());
mViewHolder.strengthAdvantage.setOnValueChangedListener(value -> mViewModel.setStrengthAdvantage(value));
mViewHolder.dexterityProficiency.setValue(mViewModel.getDexterityProficiency().getValue());
mViewHolder.dexterityProficiency.setOnValueChangedListener(value -> mViewModel.setDexterityProficiency(value));
mViewHolder.dexterityAdvantage.setValue(mViewModel.getDexterityAdvantage().getValue());
mViewHolder.dexterityAdvantage.setOnValueChangedListener(value -> mViewModel.setDexterityAdvantage(value));
mViewHolder.constitutionProficiency.setValue(mViewModel.getConstitutionProficiency().getValue());
mViewHolder.constitutionProficiency.setOnValueChangedListener(value -> mViewModel.setConstitutionProficiency(value));
mViewHolder.constitutionAdvantage.setValue(mViewModel.getConstitutionAdvantage().getValue());
mViewHolder.constitutionAdvantage.setOnValueChangedListener(value -> mViewModel.setConstitutionAdvantage(value));
mViewHolder.intelligenceProficiency.setValue(mViewModel.getIntelligenceProficiency().getValue());
mViewHolder.intelligenceProficiency.setOnValueChangedListener(value -> mViewModel.setIntelligenceProficiency(value));
mViewHolder.intelligenceAdvantage.setValue(mViewModel.getIntelligenceAdvantage().getValue());
mViewHolder.intelligenceAdvantage.setOnValueChangedListener(value -> mViewModel.setIntelligenceAdvantage(value));
mViewHolder.wisdomProficiency.setValue(mViewModel.getWisdomProficiency().getValue());
mViewHolder.wisdomProficiency.setOnValueChangedListener(value -> mViewModel.setWisdomProficiency(value));
mViewHolder.wisdomAdvantage.setValue(mViewModel.getWisdomAdvantage().getValue());
mViewHolder.wisdomAdvantage.setOnValueChangedListener(value -> mViewModel.setWisdomAdvantage(value));
mViewHolder.charismaProficiency.setValue(mViewModel.getCharismaProficiency().getValue());
mViewHolder.charismaProficiency.setOnValueChangedListener(value -> mViewModel.setCharismaProficiency(value));
mViewHolder.charismaAdvantage.setValue(mViewModel.getCharismaAdvantage().getValue());
mViewHolder.charismaAdvantage.setOnValueChangedListener(value -> mViewModel.setCharismaAdvantage(value));
return root;
}
private static class ViewHolder {
AdvantagePicker strengthAdvantage;
ProficiencyPicker strengthProficiency;
AdvantagePicker dexterityAdvantage;
ProficiencyPicker dexterityProficiency;
AdvantagePicker constitutionAdvantage;
ProficiencyPicker constitutionProficiency;
AdvantagePicker intelligenceAdvantage;
ProficiencyPicker intelligenceProficiency;
AdvantagePicker wisdomAdvantage;
ProficiencyPicker wisdomProficiency;
AdvantagePicker charismaAdvantage;
ProficiencyPicker charismaProficiency;
ViewHolder(@NonNull View root) {
strengthAdvantage = root.findViewById(R.id.strengthAdvantage);
strengthProficiency = root.findViewById(R.id.strengthProficiency);
dexterityAdvantage = root.findViewById(R.id.dexterityAdvantage);
dexterityProficiency = root.findViewById(R.id.dexterityProficiency);
constitutionAdvantage = root.findViewById(R.id.constitutionAdvantage);
constitutionProficiency = root.findViewById(R.id.constitutionProficiency);
intelligenceAdvantage = root.findViewById(R.id.intelligenceAdvantage);
intelligenceProficiency = root.findViewById(R.id.intelligenceProficiency);
wisdomAdvantage = root.findViewById(R.id.wisdomAdvantage);
wisdomProficiency = root.findViewById(R.id.wisdomProficiency);
charismaAdvantage = root.findViewById(R.id.charismaAdvantage);
charismaProficiency = root.findViewById(R.id.charismaProficiency);
}
}
}

View File

@@ -0,0 +1,99 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.ui.components.AbilityScorePicker;
import com.majinnaibu.monstercards.ui.components.AdvantagePicker;
import com.majinnaibu.monstercards.ui.components.ProficiencyPicker;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditSkillFragment extends MCFragment {
private EditMonsterViewModel mEditMonsterViewModel;
private EditSkillViewModel mViewModel;
private ViewHolder mHolder;
private Skill mOldSkill;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this).get(EditSkillViewModel.class);
if (getArguments() != null) {
EditSkillFragmentArgs args = EditSkillFragmentArgs.fromBundle(getArguments());
mOldSkill = new Skill(args.getName(), args.getAbilityScore(), args.getAdvantage(), args.getProficiency());
mViewModel.copyFromSkill(mOldSkill);
} else {
Logger.logWTF("EditSkillFragment needs arguments.");
mOldSkill = null;
}
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_skill, container, false);
mHolder = new ViewHolder(root);
mHolder.abilityScore.setValue(mViewModel.getAbilityScore().getValue());
mHolder.abilityScore.setOnValueChangedListener(value -> mViewModel.setAbilityScore(value));
mHolder.advantage.setValue(mViewModel.getAdvantage().getValue());
mHolder.advantage.setOnValueChangedListener(value -> mViewModel.setAdvantage(value));
mHolder.proficiency.setValue(mViewModel.getProficiency().getValue());
mHolder.proficiency.setOnValueChangedListener(value -> mViewModel.setProficiency(value));
mHolder.name.setText(mViewModel.getName().getValue());
mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString())));
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (mViewModel.hasChanges()) {
mEditMonsterViewModel.replaceSkill(mViewModel.getSkill().getValue(), mOldSkill);
}
Navigation.findNavController(requireView()).navigateUp();
}
});
return root;
}
@Override
public void onStart() {
super.onStart();
mHolder.name.requestFocus();
}
private static class ViewHolder {
AbilityScorePicker abilityScore;
AdvantagePicker advantage;
ProficiencyPicker proficiency;
EditText name;
ViewHolder(@NonNull View root) {
abilityScore = root.findViewById(R.id.abilityScore);
advantage = root.findViewById(R.id.advantage);
proficiency = root.findViewById(R.id.proficiency);
name = root.findViewById(R.id.name);
}
}
}

View File

@@ -0,0 +1,81 @@
package com.majinnaibu.monstercards.ui.editmonster;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import com.majinnaibu.monstercards.data.enums.AbilityScore;
import com.majinnaibu.monstercards.data.enums.AdvantageType;
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel;
import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData;
public class EditSkillViewModel extends ChangeTrackedViewModel {
private final ChangeTrackedLiveData<AbilityScore> mAbilityScore;
private final ChangeTrackedLiveData<AdvantageType> mAdvantageType;
private final ChangeTrackedLiveData<ProficiencyType> mProficiencyType;
private final ChangeTrackedLiveData<String> mName;
private final ChangeTrackedLiveData<Skill> mSkill;
public EditSkillViewModel() {
super();
mAbilityScore = new ChangeTrackedLiveData<>(AbilityScore.STRENGTH, this::makeDirty);
mAdvantageType = new ChangeTrackedLiveData<>(AdvantageType.NONE, this::makeDirty);
mProficiencyType = new ChangeTrackedLiveData<>(ProficiencyType.NONE, this::makeDirty);
mName = new ChangeTrackedLiveData<>("New Skill", this::makeDirty);
mSkill = new ChangeTrackedLiveData<>(makeSkill(), this::makeDirty);
}
public void copyFromSkill(@NonNull Skill skill) {
mAbilityScore.resetValue(skill.abilityScore);
mAdvantageType.resetValue(skill.advantageType);
mProficiencyType.resetValue(skill.proficiencyType);
mName.resetValue(skill.name);
makeClean();
}
public LiveData<Skill> getSkill() {
return mSkill;
}
public LiveData<AbilityScore> getAbilityScore() {
return mAbilityScore;
}
public void setAbilityScore(AbilityScore value) {
mAbilityScore.setValue(value);
mSkill.setValue(makeSkill());
}
public LiveData<AdvantageType> getAdvantage() {
return mAdvantageType;
}
public void setAdvantage(AdvantageType value) {
mAdvantageType.setValue(value);
mSkill.setValue(makeSkill());
}
public LiveData<ProficiencyType> getProficiency() {
return mProficiencyType;
}
public void setProficiency(ProficiencyType value) {
mProficiencyType.setValue(value);
mSkill.setValue(makeSkill());
}
public LiveData<String> getName() {
return mName;
}
public void setName(String value) {
mName.setValue(value);
mSkill.setValue(makeSkill());
}
@NonNull
private Skill makeSkill() {
return new Skill(mName.getValue(), mAbilityScore.getValue(), mAdvantageType.getValue(), mProficiencyType.getValue());
}
}

View File

@@ -0,0 +1,92 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Skill;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger;
/**
* A fragment representing a list of Items.
*/
public class EditSkillsFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
private void navigateToEditSkill(@NonNull Skill skill) {
NavDirections action = EditSkillsFragmentDirections.actionEditSkillsFragmentToEditSkillFragment(skill.name, skill.abilityScore, skill.proficiencyType, skill.advantageType);
Navigation.findNavController(requireView()).navigate(action);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_skills_list, container, false);
mHolder = new ViewHolder(root);
setupRecyclerView(mHolder.list);
setupAddSkillButton(mHolder.addSkill);
return root;
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
mViewModel.getSkills().observe(getViewLifecycleOwner(), skills -> {
EditSkillsRecyclerViewAdapter adapter = new EditSkillsRecyclerViewAdapter(mViewModel.getSkillsArray(), skill -> {
if (skill != null) {
navigateToEditSkill(skill);
} else {
Logger.logError("Can't navigate to EditSkill with a null skill");
}
});
recyclerView.setAdapter(adapter);
});
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> mViewModel.removeSkill(position), null));
itemTouchHelper.attachToRecyclerView(recyclerView);
}
private void setupAddSkillButton(@NonNull FloatingActionButton fab) {
fab.setOnClickListener(view -> {
Skill newSkill = mViewModel.addNewSkill();
navigateToEditSkill(newSkill);
});
}
private static class ViewHolder {
RecyclerView list;
FloatingActionButton addSkill;
ViewHolder(@NonNull View root) {
this.list = root.findViewById(R.id.list);
this.addSkill = root.findViewById(R.id.add_skill);
}
}
}

View File

@@ -0,0 +1,68 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.databinding.FragmentEditSkillsListItemBinding;
import com.majinnaibu.monstercards.models.Skill;
import java.util.List;
/**
* {@link RecyclerView.Adapter} that can display a {@link Skill}.
*/
public class EditSkillsRecyclerViewAdapter extends RecyclerView.Adapter<EditSkillsRecyclerViewAdapter.ViewHolder> {
private final List<Skill> mValues;
private final ItemCallback mOnClick;
public EditSkillsRecyclerViewAdapter(List<Skill> items, ItemCallback onClick) {
mValues = items;
mOnClick = onClick;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(FragmentEditSkillsListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
holder.mItem = mValues.get(position);
holder.mContentView.setText(mValues.get(position).name);
holder.itemView.setOnClickListener(v -> {
if (mOnClick != null) {
mOnClick.onItemCallback(holder.mItem);
}
});
}
@Override
public int getItemCount() {
return mValues.size();
}
public interface ItemCallback {
void onItemCallback(Skill skill);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView mContentView;
public Skill mItem;
public ViewHolder(@NonNull FragmentEditSkillsListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -0,0 +1,90 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SwitchCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.ui.components.Stepper;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditSpeedFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_speed, container, false);
mHolder = new ViewHolder(root);
mHolder.baseSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setWalkSpeed(newValue));
mHolder.baseSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value));
mViewModel.getWalkSpeed().observe(getViewLifecycleOwner(), value -> mHolder.baseSpeed.setValue(value));
mHolder.burrowSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setBurrowSpeed(newValue));
mHolder.burrowSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value));
mViewModel.getBurrowSpeed().observe(getViewLifecycleOwner(), value -> mHolder.burrowSpeed.setValue(value));
mHolder.climbSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setClimbSpeed(newValue));
mHolder.climbSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value));
mViewModel.getClimbSpeed().observe(getViewLifecycleOwner(), value -> mHolder.climbSpeed.setValue(value));
mHolder.flySpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setFlySpeed(newValue));
mHolder.flySpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value));
mViewModel.getFlySpeed().observe(getViewLifecycleOwner(), value -> mHolder.flySpeed.setValue(value));
mHolder.swimSpeed.setOnValueChangeListener((newValue, oldValue) -> mViewModel.setSwimSpeed(newValue));
mHolder.swimSpeed.setOnFormatValueCallback(value -> String.format(getString(R.string.format_distance_in_feet), value));
mViewModel.getSwimSpeed().observe(getViewLifecycleOwner(), value -> mHolder.swimSpeed.setValue(value));
mViewModel.getCanHover().observe(getViewLifecycleOwner(), value -> mHolder.canHover.setChecked(value));
mHolder.canHover.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setCanHover(isChecked));
mViewModel.getHasCustomSpeed().observe(getViewLifecycleOwner(), value -> mHolder.hasCustomSpeed.setChecked(value));
mHolder.hasCustomSpeed.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setHasCustomSpeed(isChecked));
//mViewModel.getCustomSpeed().observe(getViewLifecycleOwner(), value -> mHolder.customSpeed.setText(value));
mHolder.customSpeed.setText(mViewModel.getCustomSpeed().getValue());
mHolder.customSpeed.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setCustomSpeed(s.toString())));
return root;
}
private static class ViewHolder {
final Stepper baseSpeed;
final Stepper burrowSpeed;
final Stepper climbSpeed;
final Stepper flySpeed;
final Stepper swimSpeed;
final SwitchCompat canHover;
final SwitchCompat hasCustomSpeed;
final EditText customSpeed;
ViewHolder(@NonNull View root) {
baseSpeed = root.findViewById(R.id.baseSpeed);
burrowSpeed = root.findViewById(R.id.burrowSpeed);
climbSpeed = root.findViewById(R.id.climbSpeed);
flySpeed = root.findViewById(R.id.flySpeed);
swimSpeed = root.findViewById(R.id.swimSpeed);
canHover = root.findViewById(R.id.canHover);
hasCustomSpeed = root.findViewById(R.id.hasCustomSpeed);
customSpeed = root.findViewById(R.id.customSpeed);
}
}
}

View File

@@ -0,0 +1,102 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.StringType;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditStringFragment extends MCFragment {
private EditMonsterViewModel mEditMonsterViewModel;
private EditStringViewModel mViewModel;
private ViewHolder mHolder;
private String mOldValue;
private StringType mStringType;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this).get(EditStringViewModel.class);
if (getArguments() != null) {
EditStringFragmentArgs args = EditStringFragmentArgs.fromBundle(getArguments());
mOldValue = args.getValue();
mViewModel.setValue(mOldValue);
mStringType = args.getStringType();
} else {
Logger.logWTF("EditStringFragment needs arguments");
mOldValue = null;
}
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_string, container, false);
mHolder = new ViewHolder(root);
setTitle(getTitleForStringType(mStringType));
mHolder.description.setText(mViewModel.getValueAsString());
mHolder.description.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setValue(s.toString())));
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (mViewModel.hasChanges()) {
mEditMonsterViewModel.replaceString(mStringType, mOldValue, mViewModel.getValueAsString());
}
Navigation.findNavController(requireView()).navigateUp();
}
});
return root;
}
@NonNull
private String getTitleForStringType(@NonNull StringType type) {
switch (type) {
case CONDITION_IMMUNITY:
return getString(R.string.title_editConditionImmunity);
case DAMAGE_IMMUNITY:
return getString(R.string.title_editDamageImmunity);
case DAMAGE_RESISTANCE:
return getString(R.string.title_editDamageResistance);
case DAMAGE_VULNERABILITY:
return getString(R.string.title_editDamageVulnerability);
case SENSE:
return getString(R.string.title_editSense);
default:
return getString(R.string.title_editString);
}
}
@Override
public void onStart() {
super.onStart();
mHolder.description.requestFocus();
}
private static class ViewHolder {
EditText description;
ViewHolder(@NonNull View root) {
description = root.findViewById(R.id.description);
}
}
}

View File

@@ -0,0 +1,32 @@
package com.majinnaibu.monstercards.ui.editmonster;
import androidx.lifecycle.LiveData;
import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel;
import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData;
public class EditStringViewModel extends ChangeTrackedViewModel {
private final ChangeTrackedLiveData<String> mValue;
public EditStringViewModel() {
super();
mValue = new ChangeTrackedLiveData<>("", this::makeDirty);
}
public LiveData<String> getValue() {
return mValue;
}
public void setValue(String value) {
mValue.setValue(value);
}
public String getValueAsString() {
return mValue.getValue();
}
public void resetValue(String value) {
makeClean();
mValue.resetValue(value);
}
}

View File

@@ -0,0 +1,129 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.StringType;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.List;
public class EditStringsFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
private StringType mStringType;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Bundle arguments = getArguments();
if (arguments != null) {
EditStringsFragmentArgs args = EditStringsFragmentArgs.fromBundle(arguments);
mStringType = args.getStringType();
} else {
Logger.logWTF("EditStringsFragment needs arguments");
}
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_strings_list, container, false);
mHolder = new ViewHolder(root);
setTitle(getTitleForStringType(mStringType));
setupRecyclerView(mHolder.list);
setupAddButton(mHolder.addItem);
return root;
}
@NonNull
private String getTitleForStringType(StringType type) {
switch (type) {
case CONDITION_IMMUNITY:
return getString(R.string.title_editConditionImmunities);
case DAMAGE_IMMUNITY:
return getString(R.string.title_editDamageImmunities);
case DAMAGE_RESISTANCE:
return getString(R.string.title_editDamageResistances);
case DAMAGE_VULNERABILITY:
return getString(R.string.title_editDamageVulnerabilities);
case SENSE:
return getString(R.string.title_editSenses);
default:
return getString(R.string.title_editStrings);
}
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<String>> stringsData = mViewModel.getStrings(mStringType);
if (stringsData != null) {
stringsData.observe(getViewLifecycleOwner(), strings -> {
EditStringsRecyclerViewAdapter adapter = new EditStringsRecyclerViewAdapter(strings, value -> {
if (value != null) {
navigateToEditString(value);
} else {
Logger.logError("Can't navigate to EditStringFragment with a null trait");
}
});
recyclerView.setAdapter(adapter);
});
}
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> mViewModel.removeString(mStringType, position), null));
itemTouchHelper.attachToRecyclerView(recyclerView);
}
private void setupAddButton(@NonNull FloatingActionButton fab) {
fab.setOnClickListener(view -> {
String newValue = mViewModel.addNewString(mStringType);
if (newValue != null) {
navigateToEditString(newValue);
}
});
}
protected void navigateToEditString(String value) {
NavDirections action = EditStringsFragmentDirections.actionEditStringsFragmentToEditStringFragment(mStringType, value);
Navigation.findNavController(requireView()).navigate(action);
}
private static class ViewHolder {
RecyclerView list;
FloatingActionButton addItem;
ViewHolder(@NonNull View root) {
list = root.findViewById(R.id.list);
addItem = root.findViewById(R.id.add_item);
}
}
}

View File

@@ -0,0 +1,64 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.databinding.FragmentEditStringsListItemBinding;
import java.util.List;
public class EditStringsRecyclerViewAdapter extends RecyclerView.Adapter<EditStringsRecyclerViewAdapter.ViewHolder> {
private final List<String> mValues;
private final ItemCallback mOnClick;
public EditStringsRecyclerViewAdapter(List<String> items, ItemCallback onClick) {
mValues = items;
mOnClick = onClick;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(FragmentEditStringsListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
holder.mItem = mValues.get(position);
holder.mContentView.setText(mValues.get(position));
holder.itemView.setOnClickListener(v -> {
if (mOnClick != null) {
mOnClick.onItemCallback(holder.mItem);
}
});
}
@Override
public int getItemCount() {
return mValues.size();
}
public interface ItemCallback {
void onItemCallback(String value);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView mContentView;
public String mItem;
public ViewHolder(@NonNull FragmentEditStringsListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -0,0 +1,110 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.TraitType;
import com.majinnaibu.monstercards.models.Trait;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import com.majinnaibu.monstercards.utils.TextChangedListener;
public class EditTraitFragment extends MCFragment {
private EditMonsterViewModel mEditMonsterViewModel;
private EditTraitViewModel mViewModel;
private EditTraitFragment.ViewHolder mHolder;
private Trait mOldValue;
private TraitType mTraitType;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this).get(EditTraitViewModel.class);
if (getArguments() != null) {
EditTraitFragmentArgs args = EditTraitFragmentArgs.fromBundle(getArguments());
mOldValue = new Trait(args.getName(), args.getDescription());
mViewModel.copyFromTrait(mOldValue);
mTraitType = args.getTraitType();
} else {
Logger.logWTF("EditTraitFragment needs arguments");
mOldValue = null;
}
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_trait, container, false);
mHolder = new EditTraitFragment.ViewHolder(root);
setTitle(getTitleForTraitType(mTraitType));
mHolder.name.setText(mViewModel.getNameAsString());
mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString())));
mHolder.description.setText(mViewModel.getDescriptionAsString());
mHolder.description.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setDescription(s.toString())));
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (mViewModel.hasChanges()) {
mEditMonsterViewModel.replaceTrait(mTraitType, mOldValue, mViewModel.getAbilityValue());
}
Navigation.findNavController(requireView()).navigateUp();
}
});
return root;
}
@Override
public void onStart() {
super.onStart();
mHolder.name.requestFocus();
}
private String getTitleForTraitType(TraitType type) {
switch (type) {
case ABILITY:
return getString(R.string.title_editAbility);
case ACTION:
return getString(R.string.title_editAction);
case LAIR_ACTION:
return getString(R.string.title_editLairAction);
case LEGENDARY_ACTION:
return getString(R.string.title_editLegendaryAction);
case REACTIONS:
return getString(R.string.title_editReaction);
case REGIONAL_ACTION:
return getString(R.string.title_editRegionalAction);
default:
return getString(R.string.title_editTrait);
}
}
private static class ViewHolder {
EditText description;
EditText name;
ViewHolder(@NonNull View root) {
description = root.findViewById(R.id.description);
name = root.findViewById(R.id.name);
}
}
}

View File

@@ -0,0 +1,63 @@
package com.majinnaibu.monstercards.ui.editmonster;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.majinnaibu.monstercards.models.Trait;
import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel;
import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData;
public class EditTraitViewModel extends ChangeTrackedViewModel {
private final ChangeTrackedLiveData<String> mName;
private final ChangeTrackedLiveData<String> mDescription;
private final MutableLiveData<Trait> mAbility;
public EditTraitViewModel() {
super();
mName = new ChangeTrackedLiveData<>("", this::makeDirty);
mDescription = new ChangeTrackedLiveData<>("", this::makeDirty);
mAbility = new MutableLiveData<>(makeAbility());
}
public LiveData<String> getName() {
return mName;
}
public void setName(String name) {
mName.setValue(name);
mAbility.setValue(makeAbility());
}
public String getNameAsString() {
return mName.getValue();
}
public LiveData<String> getDescription() {
return mDescription;
}
public void setDescription(String description) {
mDescription.setValue(description);
mAbility.setValue(makeAbility());
}
public String getDescriptionAsString() {
return mDescription.getValue();
}
public Trait getAbilityValue() {
return mAbility.getValue();
}
public void copyFromTrait(@NonNull Trait trait) {
makeClean();
mName.resetValue(trait.name);
mDescription.resetValue(trait.description);
}
@NonNull
private Trait makeAbility() {
return new Trait(mName.getValue(), mDescription.getValue());
}
}

View File

@@ -0,0 +1,131 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.enums.TraitType;
import com.majinnaibu.monstercards.models.Trait;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.List;
public class EditTraitsFragment extends MCFragment {
private EditMonsterViewModel mViewModel;
private ViewHolder mHolder;
private TraitType mTraitType;
private EditTraitsRecyclerViewAdapter mAdapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
if (getArguments() != null) {
EditTraitsFragmentArgs args = EditTraitsFragmentArgs.fromBundle(getArguments());
mTraitType = args.getTraitType();
} else {
Logger.logWTF("EditTraitFragment needs arguments");
}
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_edit_traits_list, container, false);
mHolder = new ViewHolder(root);
setTitle(getTitleForTraitType(mTraitType));
setupRecyclerView(mHolder.list);
setupAddButton(mHolder.addTrait);
return root;
}
@NonNull
private String getTitleForTraitType(TraitType type) {
switch (type) {
case ABILITY:
return getString(R.string.title_editAbilities);
case ACTION:
return getString(R.string.title_editActions);
case LAIR_ACTION:
return getString(R.string.title_editLairActions);
case LEGENDARY_ACTION:
return getString(R.string.title_editLegendaryActions);
case REACTIONS:
return getString(R.string.title_editReactions);
case REGIONAL_ACTION:
return getString(R.string.title_editRegionalActions);
default:
return getString(R.string.title_editTraits);
}
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
LiveData<List<Trait>> traitData = mViewModel.getTraits(mTraitType);
mAdapter = new EditTraitsRecyclerViewAdapter(trait -> {
if (trait != null) {
navigateToEditTrait(trait);
} else {
Logger.logError("Can't navigate to EditTraitFragment with a null trait");
}
});
if (traitData != null) {
traitData.observe(getViewLifecycleOwner(), traits -> mAdapter.submitList(traits));
}
recyclerView.setAdapter(mAdapter);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, (position, direction) -> mViewModel.removeTrait(mTraitType, position), (from, to) -> mViewModel.moveTrait(mTraitType, from, to)));
itemTouchHelper.attachToRecyclerView(recyclerView);
}
private void setupAddButton(@NonNull FloatingActionButton fab) {
fab.setOnClickListener(view -> {
Trait newTrait = mViewModel.addNewTrait(mTraitType);
if (newTrait != null) {
navigateToEditTrait(newTrait);
}
});
}
protected void navigateToEditTrait(@NonNull Trait trait) {
NavDirections action = EditTraitsFragmentDirections.actionEditTraitListFragmentToEditTraitFragment(trait.description, trait.name, mTraitType);
Navigation.findNavController(requireView()).navigate(action);
}
private static class ViewHolder {
RecyclerView list;
FloatingActionButton addTrait;
ViewHolder(@NonNull View root) {
list = root.findViewById(R.id.list);
addTrait = root.findViewById(R.id.add_trait);
}
}
}

View File

@@ -0,0 +1,71 @@
package com.majinnaibu.monstercards.ui.editmonster;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.databinding.FragmentEditTraitsListItemBinding;
import com.majinnaibu.monstercards.models.Trait;
public class EditTraitsRecyclerViewAdapter extends ListAdapter<Trait, EditTraitsRecyclerViewAdapter.ViewHolder> {
private static final DiffUtil.ItemCallback<Trait> DIFF_CALLBACK = new DiffUtil.ItemCallback<Trait>() {
@Override
public boolean areItemsTheSame(@NonNull Trait oldItem, @NonNull Trait newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areContentsTheSame(@NonNull Trait oldItem, @NonNull Trait newItem) {
return oldItem.equals(newItem);
}
};
private final ItemCallback mOnClick;
protected EditTraitsRecyclerViewAdapter(ItemCallback onClick) {
super(DIFF_CALLBACK);
mOnClick = onClick;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(FragmentEditTraitsListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
holder.mItem = getItem(position);
holder.mContentView.setText(holder.mItem.name);
holder.itemView.setOnClickListener(v -> {
if (mOnClick != null) {
mOnClick.onItemCallback(holder.mItem);
}
});
}
public interface ItemCallback {
void onItemCallback(Trait trait);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView mContentView;
public Trait mItem;
public ViewHolder(@NonNull FragmentEditTraitsListItemBinding binding) {
super(binding.getRoot());
mContentView = binding.content;
}
@NonNull
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}

View File

@@ -0,0 +1,121 @@
package com.majinnaibu.monstercards.ui.library;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class LibraryFragment extends MCFragment {
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_library, container, false);
FloatingActionButton fab = root.findViewById(R.id.fab);
assert fab != null;
setupAddMonsterButton(fab);
final RecyclerView recyclerView = root.findViewById(R.id.monster_list);
assert recyclerView != null;
setupRecyclerView(recyclerView);
return root;
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
Context context = requireContext();
MonsterRepository repository = this.getMonsterRepository();
LibraryRecyclerViewAdapter adapter = new LibraryRecyclerViewAdapter(
context,
repository.getMonsters(),
(monster) -> navigateToMonsterDetail(monster.id),
(monster) -> repository
.deleteMonster(monster)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError(e);
}
}));
recyclerView.setAdapter(adapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(requireContext(), (position, direction) -> adapter.deleteItem(position), null));
itemTouchHelper.attachToRecyclerView(recyclerView);
}
private void setupAddMonsterButton(@NonNull FloatingActionButton fab) {
fab.setOnClickListener(view -> {
Monster monster = new Monster();
monster.name = getString(R.string.default_monster_name);
MonsterRepository repository = this.getMonsterRepository();
repository.addMonster(monster)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
new DisposableCompletableObserver() {
@Override
public void onComplete() {
View view = getView();
assert view != null;
Snackbar.make(
view,
getString(R.string.snackbar_monster_created, monster.name),
Snackbar.LENGTH_LONG)
.setAction("Action", (_view) -> navigateToMonsterDetail(monster.id))
.show();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error creating monster", e);
View view = getView();
assert view != null;
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
});
}
protected void navigateToMonsterDetail(@NonNull UUID monsterId) {
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
Navigation.findNavController(requireView()).navigate(action);
}
}

View File

@@ -0,0 +1,115 @@
package com.majinnaibu.monstercards.ui.library;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Monster;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class LibraryRecyclerViewAdapter extends RecyclerView.Adapter<LibraryRecyclerViewAdapter.ViewHolder> {
private final Context mContext;
private final ItemCallback mOnDelete;
private final ItemCallback mOnClick;
private final Flowable<List<Monster>> mItemsObservable;
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(@NonNull View view) {
Monster monster = (Monster) view.getTag();
if (mOnClick != null) {
mOnClick.onItemCallback(monster);
}
}
};
private List<Monster> mValues;
private Disposable mDisposable;
public LibraryRecyclerViewAdapter(Context context,
Flowable<List<Monster>> itemsObservable,
ItemCallback onClick,
ItemCallback onDelete) {
mItemsObservable = itemsObservable;
mValues = new ArrayList<>();
mContext = context;
mOnDelete = onDelete;
mOnClick = onClick;
mDisposable = null;
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.monster_list_content, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final @NonNull ViewHolder holder, int position) {
Monster monster = mValues.get(position);
holder.mContentView.setText(monster.name);
holder.itemView.setTag(monster);
holder.itemView.setOnClickListener(mOnClickListener);
}
@Override
public int getItemCount() {
return mValues.size();
}
public Context getContext() {
return mContext;
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
// TODO: consider moving this subscription out of the adapter and make the subscriber call setItems on the adapter
mDisposable = mItemsObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
mValues = monsters;
notifyDataSetChanged();
});
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
mDisposable.dispose();
}
public void deleteItem(int position) {
if (mOnDelete != null) {
Monster monster = mValues.get(position);
mOnDelete.onItemCallback(monster);
}
}
public interface ItemCallback {
void onItemCallback(Monster monster);
}
static class ViewHolder extends RecyclerView.ViewHolder {
final TextView mContentView;
ViewHolder(View view) {
super(view);
mContentView = view.findViewById(R.id.content);
}
}
}

View File

@@ -0,0 +1,256 @@
package com.majinnaibu.monstercards.ui.monster;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.List;
import java.util.UUID;
import io.reactivex.rxjava3.observers.DisposableSingleObserver;
public class MonsterDetailFragment extends MCFragment {
private ViewHolder mHolder;
private MonsterDetailViewModel mViewModel;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
MonsterRepository repository = getMonsterRepository();
Bundle arguments = getArguments();
assert arguments != null;
UUID monsterId = UUID.fromString(MonsterDetailFragmentArgs.fromBundle(arguments).getMonsterId());
setHasOptionsMenu(true);
mViewModel = new ViewModelProvider(this).get(MonsterDetailViewModel.class);
repository.getMonster(monsterId).toObservable()
.firstOrError()
.subscribe(new DisposableSingleObserver<Monster>() {
@Override
public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull Monster monster) {
mViewModel.setMonster(monster);
dispose();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError(e);
dispose();
}
});
View root = inflater.inflate(R.layout.fragment_monster, container, false);
mHolder = new ViewHolder(root);
mViewModel.getName().observe(getViewLifecycleOwner(), name -> {
mHolder.name.setText(name);
setTitle(getString(R.string.title_monsterDetails_fmt, name));
});
mViewModel.getMeta().observe(getViewLifecycleOwner(), mHolder.meta::setText);
mViewModel.getArmorClass().observe(getViewLifecycleOwner(), armorText -> setupLabeledTextView(mHolder.armorClass, armorText, R.string.label_armor_class));
mViewModel.getHitPoints().observe(getViewLifecycleOwner(), hitPoints -> setupLabeledTextView(mHolder.hitPoints, hitPoints, R.string.label_hit_points));
mViewModel.getSpeed().observe(getViewLifecycleOwner(), speed -> setupLabeledTextView(mHolder.speed, speed, R.string.label_speed));
mViewModel.getStrength().observe(getViewLifecycleOwner(), mHolder.strength::setText);
mViewModel.getDexterity().observe(getViewLifecycleOwner(), mHolder.dexterity::setText);
mViewModel.getConstitution().observe(getViewLifecycleOwner(), mHolder.constitution::setText);
mViewModel.getIntelligence().observe(getViewLifecycleOwner(), mHolder.intelligence::setText);
mViewModel.getWisdom().observe(getViewLifecycleOwner(), mHolder.wisdom::setText);
mViewModel.getCharisma().observe(getViewLifecycleOwner(), mHolder.charisma::setText);
mViewModel.getSavingThrows().observe(getViewLifecycleOwner(), savingThrows -> setupOptionalTextView(mHolder.savingThrows, savingThrows, R.string.label_saving_throws));
mViewModel.getSkills().observe(getViewLifecycleOwner(), skills -> setupOptionalTextView(mHolder.skills, skills, R.string.label_skills));
mViewModel.getDamageVulnerabilities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageVulnerabilities, damageTypes, R.string.label_damage_vulnerabilities));
mViewModel.getDamageResistances().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageResistances, damageTypes, R.string.label_damage_resistances));
mViewModel.getDamageImmunities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageImmunities, damageTypes, R.string.label_damage_immunities));
mViewModel.getConditionImmunities().observe(getViewLifecycleOwner(), conditionImmunities -> setupOptionalTextView(mHolder.conditionImmunities, conditionImmunities, R.string.label_condition_immunities));
mViewModel.getSenses().observe(getViewLifecycleOwner(), senses -> setupOptionalTextView(mHolder.senses, senses, R.string.label_senses));
mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> setupOptionalTextView(mHolder.languages, languages, R.string.label_languages));
mViewModel.getChallenge().observe(getViewLifecycleOwner(), challengeRating -> setupLabeledTextView(mHolder.challenge, challengeRating, R.string.label_challenge_rating));
mViewModel.getAbilities().observe(getViewLifecycleOwner(), abilities -> setupTraitList(mHolder.abilities, abilities));
mViewModel.getActions().observe(getViewLifecycleOwner(), actions -> setupTraitList(mHolder.actions, actions, mHolder.actions_label, mHolder.actions_divider));
mViewModel.getReactions().observe(getViewLifecycleOwner(), reactions -> setupTraitList(mHolder.reactions, reactions, mHolder.reactions_label, mHolder.reactions_divider));
mViewModel.getRegionalEffects().observe(getViewLifecycleOwner(), regionalEffects -> setupTraitList(mHolder.regionalEffects, regionalEffects, mHolder.regionalEffects_label, mHolder.regionalEffects_divider));
mViewModel.getLairActions().observe(getViewLifecycleOwner(), lairActions -> setupTraitList(mHolder.lairActions, lairActions, mHolder.lairActions_label, mHolder.lairActions_divider));
mViewModel.getLegendaryActions().observe(getViewLifecycleOwner(), legendaryActions -> setupTraitList(mHolder.legendaryActions, legendaryActions, mHolder.legendaryActions_label, mHolder.legendaryActions_divider));
return root;
}
private void setupLabeledTextView(@NonNull TextView view, String text, int titleId) {
String title = getString(titleId);
String fullText = String.format("<b>%s</b> %s", title, text);
view.setText(Html.fromHtml(fullText));
}
private void setupOptionalTextView(TextView root, String text, int titleId) {
String title = getString(titleId);
if (StringHelper.isNullOrEmpty(text)) {
root.setVisibility(View.GONE);
} else {
root.setVisibility(View.VISIBLE);
}
Spanned formatted;
if (StringHelper.isNullOrEmpty(title)) {
formatted = Html.fromHtml(text);
} else {
formatted = Html.fromHtml(String.format("<b>%s</b> %s", title, text));
}
root.setText(formatted);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits) {
setupTraitList(root, traits, null, null);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits, View label, View divider) {
int visibility = traits.size() > 0 ? View.VISIBLE : View.GONE;
Context context = getContext();
DisplayMetrics displayMetrics = null;
if (context != null) {
Resources resources = context.getResources();
if (resources != null) {
displayMetrics = resources.getDisplayMetrics();
}
}
root.removeAllViews();
for (String action : traits) {
TextView tvAction = new TextView(getContext());
// TODO: Handle multiline block quotes specially so they stay multiline.
// TODO: Replace QuoteSpans in the result of fromHtml with something like this https://stackoverflow.com/questions/7717567/how-to-style-blockquotes-in-android-textviews to make them indent as expected
tvAction.setText(Html.fromHtml(CommonMarkHelper.toHtml(action)));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, displayMetrics);
tvAction.setLayoutParams(layoutParams);
root.addView(tvAction);
}
root.setVisibility(visibility);
if (label != null) {
label.setVisibility(visibility);
}
if (divider != null) {
divider.setVisibility(visibility);
}
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.monster_detail_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_action_edit_monster) {
UUID monsterId = mViewModel.getId().getValue();
if (monsterId != null) {
NavDirections action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
Navigation.findNavController(requireView()).navigate(action);
} else {
Logger.logWTF("monsterId cannot be null.");
}
return true;
}
return super.onOptionsItemSelected(item);
}
private static class ViewHolder {
final TextView name;
final TextView meta;
final TextView armorClass;
final TextView hitPoints;
final TextView speed;
final TextView strength;
final TextView dexterity;
final TextView constitution;
final TextView intelligence;
final TextView wisdom;
final TextView charisma;
final TextView savingThrows;
final TextView skills;
final TextView damageVulnerabilities;
final TextView damageResistances;
final TextView damageImmunities;
final TextView conditionImmunities;
final TextView senses;
final TextView languages;
final TextView challenge;
final LinearLayout abilities;
final LinearLayout actions;
final TextView actions_label;
final ImageView actions_divider;
final LinearLayout reactions;
final TextView reactions_label;
final ImageView reactions_divider;
final LinearLayout legendaryActions;
final TextView legendaryActions_label;
final ImageView legendaryActions_divider;
final LinearLayout lairActions;
final TextView lairActions_label;
final ImageView lairActions_divider;
final LinearLayout regionalEffects;
final TextView regionalEffects_label;
final ImageView regionalEffects_divider;
ViewHolder(@NonNull View root) {
name = root.findViewById(R.id.name);
meta = root.findViewById(R.id.meta);
armorClass = root.findViewById(R.id.armorClass);
hitPoints = root.findViewById(R.id.hitPoints);
speed = root.findViewById(R.id.speed);
strength = root.findViewById(R.id.strength);
dexterity = root.findViewById(R.id.dexterity);
constitution = root.findViewById(R.id.constitution);
intelligence = root.findViewById(R.id.intelligence);
wisdom = root.findViewById(R.id.wisdom);
charisma = root.findViewById(R.id.charisma);
savingThrows = root.findViewById(R.id.savingThrows);
skills = root.findViewById(R.id.skills);
damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities);
damageResistances = root.findViewById(R.id.damageResistances);
damageImmunities = root.findViewById(R.id.damageImmunities);
conditionImmunities = root.findViewById(R.id.conditionImmunities);
senses = root.findViewById(R.id.senses);
languages = root.findViewById(R.id.languages);
challenge = root.findViewById(R.id.challenge);
abilities = root.findViewById(R.id.abilities);
actions = root.findViewById(R.id.actions);
actions_divider = root.findViewById(R.id.actions_divider);
actions_label = root.findViewById(R.id.actions_label);
reactions = root.findViewById(R.id.reactions);
reactions_divider = root.findViewById(R.id.reactions_divider);
reactions_label = root.findViewById(R.id.reactions_label);
legendaryActions = root.findViewById(R.id.legendaryActions);
legendaryActions_divider = root.findViewById(R.id.legendaryActions_divider);
legendaryActions_label = root.findViewById(R.id.legendaryActions_label);
lairActions = root.findViewById(R.id.lairActions);
lairActions_divider = root.findViewById(R.id.lairActions_divider);
lairActions_label = root.findViewById(R.id.lairActions_label);
regionalEffects = root.findViewById(R.id.regionalEffects);
regionalEffects_divider = root.findViewById(R.id.regionalEffects_divider);
regionalEffects_label = root.findViewById(R.id.regionalEffects_label);
}
}
}

View File

@@ -0,0 +1,214 @@
package com.majinnaibu.monstercards.ui.monster;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.majinnaibu.monstercards.models.Monster;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MonsterDetailViewModel extends ViewModel {
private final MutableLiveData<List<String>> mAbilities;
private final MutableLiveData<List<String>> mActions;
private final MutableLiveData<String> mArmorClass;
private final MutableLiveData<String> mChallenge;
private final MutableLiveData<String> mCharisma;
private final MutableLiveData<String> mConditionImmunities;
private final MutableLiveData<String> mConstitution;
private final MutableLiveData<String> mDamageResistances;
private final MutableLiveData<String> mDamageImmunities;
private final MutableLiveData<String> mDamageVulnerabilities;
private final MutableLiveData<String> mDexterity;
private final MutableLiveData<String> mHitPoints;
private final MutableLiveData<String> mIntelligence;
private final MutableLiveData<List<String>> mLairActions;
private final MutableLiveData<String> mLanguages;
private final MutableLiveData<List<String>> mLegendaryActions;
private final MutableLiveData<String> mMeta;
private final MutableLiveData<String> mName;
private final MutableLiveData<List<String>> mReactions;
private final MutableLiveData<List<String>> mRegionalEffects;
private final MutableLiveData<String> mSavingThrows;
private final MutableLiveData<String> mSenses;
private final MutableLiveData<String> mSkills;
private final MutableLiveData<String> mSpeed;
private final MutableLiveData<String> mStrength;
private final MutableLiveData<String> mWisdom;
private final MutableLiveData<UUID> mMonsterId;
private Monster mMonster;
public MonsterDetailViewModel() {
mMonster = null;
mAbilities = new MutableLiveData<>(new ArrayList<>());
mActions = new MutableLiveData<>(new ArrayList<>());
mArmorClass = new MutableLiveData<>("");
mChallenge = new MutableLiveData<>("");
mCharisma = new MutableLiveData<>("");
mConditionImmunities = new MutableLiveData<>("");
mConstitution = new MutableLiveData<>("");
mDamageImmunities = new MutableLiveData<>("");
mDamageResistances = new MutableLiveData<>("");
mDamageVulnerabilities = new MutableLiveData<>("");
mDexterity = new MutableLiveData<>("");
mHitPoints = new MutableLiveData<>("");
mIntelligence = new MutableLiveData<>("");
mLairActions = new MutableLiveData<>(new ArrayList<>());
mLanguages = new MutableLiveData<>("");
mLegendaryActions = new MutableLiveData<>(new ArrayList<>());
mMeta = new MutableLiveData<>("");
mName = new MutableLiveData<>("");
mReactions = new MutableLiveData<>(new ArrayList<>());
mRegionalEffects = new MutableLiveData<>(new ArrayList<>());
mSavingThrows = new MutableLiveData<>("");
mSenses = new MutableLiveData<>("");
mSkills = new MutableLiveData<>("");
mSpeed = new MutableLiveData<>("");
mStrength = new MutableLiveData<>("");
mWisdom = new MutableLiveData<>("");
mMonsterId = new MutableLiveData<>(UUID.fromString("00000000-0000-0000-0000-000000000000"));
}
public LiveData<List<String>> getAbilities() {
return mAbilities;
}
public LiveData<List<String>> getActions() {
return mActions;
}
public LiveData<List<String>> getReactions() {
return mReactions;
}
public LiveData<List<String>> getLegendaryActions() {
return mLegendaryActions;
}
public LiveData<List<String>> getLairActions() {
return mLairActions;
}
public LiveData<List<String>> getRegionalEffects() {
return mRegionalEffects;
}
public LiveData<String> getArmorClass() {
return mArmorClass;
}
public LiveData<String> getChallenge() {
return mChallenge;
}
public LiveData<String> getCharisma() {
return mCharisma;
}
public LiveData<String> getConditionImmunities() {
return mConditionImmunities;
}
public LiveData<String> getConstitution() {
return mConstitution;
}
public LiveData<String> getDamageResistances() {
return mDamageResistances;
}
public LiveData<String> getDamageImmunities() {
return mDamageImmunities;
}
public LiveData<String> getDamageVulnerabilities() {
return mDamageVulnerabilities;
}
public LiveData<String> getDexterity() {
return mDexterity;
}
public LiveData<String> getHitPoints() {
return mHitPoints;
}
public LiveData<String> getIntelligence() {
return mIntelligence;
}
public LiveData<String> getLanguages() {
return mLanguages;
}
public LiveData<String> getMeta() {
return mMeta;
}
public LiveData<String> getName() {
return mName;
}
public LiveData<String> getSavingThrows() {
return mSavingThrows;
}
public LiveData<String> getSenses() {
return mSenses;
}
public LiveData<String> getSkills() {
return mSkills;
}
public LiveData<String> getSpeed() {
return mSpeed;
}
public LiveData<String> getStrength() {
return mStrength;
}
public LiveData<String> getWisdom() {
return mWisdom;
}
public LiveData<UUID> getId() {
return mMonsterId;
}
public void setMonster(@NonNull Monster monster) {
mMonster = monster;
mAbilities.setValue(mMonster.getAbilityDescriptions());
mActions.setValue(mMonster.getActionDescriptions());
mArmorClass.setValue(mMonster.getArmorClass());
mChallenge.setValue(mMonster.getChallengeRatingDescription());
mCharisma.setValue(monster.getCharismaDescription());
mConditionImmunities.setValue(mMonster.getConditionImmunitiesDescription());
mConstitution.setValue(monster.getConstitutionDescription());
mDamageImmunities.setValue(mMonster.getDamageImmunitiesDescription());
mDamageResistances.setValue(mMonster.getDamageResistancesDescription());
mDamageVulnerabilities.setValue(mMonster.getDamageVulnerabilitiesDescription());
mDexterity.setValue(monster.getDexterityDescription());
mHitPoints.setValue(mMonster.getHitPoints());
mIntelligence.setValue(monster.getIntelligenceDescription());
mLairActions.setValue(mMonster.getLairActionDescriptions());
mLanguages.setValue(mMonster.getLanguagesDescription());
mLegendaryActions.setValue(mMonster.getLegendaryActionDescriptions());
mMeta.setValue(mMonster.getMeta());
mMonsterId.setValue(mMonster.id);
mName.setValue(mMonster.name);
mReactions.setValue(monster.getReactionDescriptions());
mRegionalEffects.setValue(monster.getRegionalActionDescriptions());
mSavingThrows.setValue(monster.getSavingThrowsDescription());
mSenses.setValue(monster.getSensesDescription());
mSkills.setValue(monster.getSkillsDescription());
mSpeed.setValue(mMonster.getSpeedText());
mStrength.setValue(monster.getStrengthDescription());
mWisdom.setValue(monster.getWisdomDescription());
}
}

View File

@@ -0,0 +1,278 @@
package com.majinnaibu.monstercards.ui.monster;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.MonsterCardsApplication;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.library.LibraryFragmentDirections;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.List;
import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class MonsterImportFragment extends MCFragment {
private ViewHolder mHolder;
private MonsterImportViewModel mViewModel;
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Logger.logDebug("MonsterCards: loading monster for import");
Bundle arguments = getArguments();
assert arguments != null;
String json = MonsterImportFragmentArgs.fromBundle(arguments).getJson();
setHasOptionsMenu(true);
Monster monster = MonsterImportHelper.fromJSON(json);
mViewModel = new ViewModelProvider(this).get(MonsterImportViewModel.class);
mViewModel.setMonster(monster);
View root = inflater.inflate(R.layout.fragment_monster, container, false);
mHolder = new ViewHolder(root);
mViewModel.getName().observe(getViewLifecycleOwner(), mHolder.name::setText);
mViewModel.getMeta().observe(getViewLifecycleOwner(), mHolder.meta::setText);
mViewModel.getArmorClass().observe(getViewLifecycleOwner(), armorText -> setupLabeledTextView(mHolder.armorClass, armorText, R.string.label_armor_class));
mViewModel.getHitPoints().observe(getViewLifecycleOwner(), hitPoints -> setupLabeledTextView(mHolder.hitPoints, hitPoints, R.string.label_hit_points));
mViewModel.getSpeed().observe(getViewLifecycleOwner(), speed -> setupLabeledTextView(mHolder.speed, speed, R.string.label_speed));
mViewModel.getStrength().observe(getViewLifecycleOwner(), mHolder.strength::setText);
mViewModel.getDexterity().observe(getViewLifecycleOwner(), mHolder.dexterity::setText);
mViewModel.getConstitution().observe(getViewLifecycleOwner(), mHolder.constitution::setText);
mViewModel.getIntelligence().observe(getViewLifecycleOwner(), mHolder.intelligence::setText);
mViewModel.getWisdom().observe(getViewLifecycleOwner(), mHolder.wisdom::setText);
mViewModel.getCharisma().observe(getViewLifecycleOwner(), mHolder.charisma::setText);
mViewModel.getSavingThrows().observe(getViewLifecycleOwner(), savingThrows -> setupOptionalTextView(mHolder.savingThrows, savingThrows, R.string.label_saving_throws));
mViewModel.getSkills().observe(getViewLifecycleOwner(), skills -> setupOptionalTextView(mHolder.skills, skills, R.string.label_skills));
mViewModel.getDamageVulnerabilities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageVulnerabilities, damageTypes, R.string.label_damage_vulnerabilities));
mViewModel.getDamageResistances().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageResistances, damageTypes, R.string.label_damage_resistances));
mViewModel.getDamageImmunities().observe(getViewLifecycleOwner(), damageTypes -> setupOptionalTextView(mHolder.damageImmunities, damageTypes, R.string.label_damage_immunities));
mViewModel.getConditionImmunities().observe(getViewLifecycleOwner(), conditionImmunities -> setupOptionalTextView(mHolder.conditionImmunities, conditionImmunities, R.string.label_condition_immunities));
mViewModel.getSenses().observe(getViewLifecycleOwner(), senses -> setupOptionalTextView(mHolder.senses, senses, R.string.label_senses));
mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> setupOptionalTextView(mHolder.languages, languages, R.string.label_languages));
mViewModel.getChallenge().observe(getViewLifecycleOwner(), challengeRating -> setupLabeledTextView(mHolder.challenge, challengeRating, R.string.label_challenge_rating));
mViewModel.getAbilities().observe(getViewLifecycleOwner(), abilities -> setupTraitList(mHolder.abilities, abilities));
mViewModel.getActions().observe(getViewLifecycleOwner(), actions -> setupTraitList(mHolder.actions, actions, mHolder.actions_label, mHolder.actions_divider));
mViewModel.getReactions().observe(getViewLifecycleOwner(), reactions -> setupTraitList(mHolder.reactions, reactions, mHolder.reactions_label, mHolder.reactions_divider));
mViewModel.getRegionalEffects().observe(getViewLifecycleOwner(), regionalEffects -> setupTraitList(mHolder.regionalEffects, regionalEffects, mHolder.regionalEffects_label, mHolder.regionalEffects_divider));
mViewModel.getLairActions().observe(getViewLifecycleOwner(), lairActions -> setupTraitList(mHolder.lairActions, lairActions, mHolder.lairActions_label, mHolder.lairActions_divider));
mViewModel.getLegendaryActions().observe(getViewLifecycleOwner(), legendaryActions -> setupTraitList(mHolder.legendaryActions, legendaryActions, mHolder.legendaryActions_label, mHolder.legendaryActions_divider));
return root;
}
private void setupLabeledTextView(@NonNull TextView view, String text, int titleId) {
String title = getString(titleId);
String fullText = String.format("<b>%s</b> %s", title, text);
view.setText(Html.fromHtml(fullText));
}
private void setupOptionalTextView(TextView root, String text, int titleId) {
String title = getString(titleId);
if (StringHelper.isNullOrEmpty(text)) {
root.setVisibility(View.GONE);
} else {
root.setVisibility(View.VISIBLE);
}
Spanned formatted;
if (StringHelper.isNullOrEmpty(title)) {
formatted = Html.fromHtml(text);
} else {
formatted = Html.fromHtml(String.format("<b>%s</b> %s", title, text));
}
root.setText(formatted);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits) {
setupTraitList(root, traits, null, null);
}
private void setupTraitList(@NonNull LinearLayout root, @NonNull List<String> traits, View label, View divider) {
int visibility = traits.size() > 0 ? View.VISIBLE : View.GONE;
Context context = getContext();
DisplayMetrics displayMetrics = null;
if (context != null) {
Resources resources = context.getResources();
if (resources != null) {
displayMetrics = resources.getDisplayMetrics();
}
}
root.removeAllViews();
for (String action : traits) {
TextView tvAction = new TextView(getContext());
// TODO: Handle multiline block quotes specially so they stay multiline.
// TODO: Replace QuoteSpans in the result of fromHtml with something like this https://stackoverflow.com/questions/7717567/how-to-style-blockquotes-in-android-textviews to make them indent as expected
tvAction.setText(Html.fromHtml(CommonMarkHelper.toHtml(action)));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, displayMetrics);
tvAction.setLayoutParams(layoutParams);
root.addView(tvAction);
}
root.setVisibility(visibility);
if (label != null) {
label.setVisibility(visibility);
}
if (divider != null) {
divider.setVisibility(visibility);
}
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.import_monster, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_action_import_monster) {
Logger.logDebug("Menu Item Selected");
Monster monster = mViewModel.getMonster();
if (monster != null) {
monster.id = UUID.randomUUID();
MonsterCardsApplication application = (MonsterCardsApplication) getApplication();
MonsterRepository repository = application.getMonsterRepository();
repository.addMonster(monster).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
Snackbar.make(
mHolder.root,
getString(R.string.snackbar_monster_created, monster.name),
Snackbar.LENGTH_LONG)
.setAction("Action", (_view) -> navigateToEditMonster(monster.id))
.show();
}
@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error creating monster", e);
Snackbar.make(mHolder.root, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
}
});
} else {
Logger.logWTF("monsterId cannot be null.");
}
return true;
}
return super.onOptionsItemSelected(item);
}
private void navigateToEditMonster(@NonNull UUID monsterId) {
NavController navController = Navigation.findNavController(requireView());
NavDirections action;
action = MonsterImportFragmentDirections.actionMonsterImportFragmentToNavigationLibrary();
navController.navigate(action);
action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
navController.navigate(action);
action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
navController.navigate(action);
}
private static class ViewHolder {
final View root;
final TextView name;
final TextView meta;
final TextView armorClass;
final TextView hitPoints;
final TextView speed;
final TextView strength;
final TextView dexterity;
final TextView constitution;
final TextView intelligence;
final TextView wisdom;
final TextView charisma;
final TextView savingThrows;
final TextView skills;
final TextView damageVulnerabilities;
final TextView damageResistances;
final TextView damageImmunities;
final TextView conditionImmunities;
final TextView senses;
final TextView languages;
final TextView challenge;
final LinearLayout abilities;
final LinearLayout actions;
final TextView actions_label;
final ImageView actions_divider;
final LinearLayout reactions;
final TextView reactions_label;
final ImageView reactions_divider;
final LinearLayout legendaryActions;
final TextView legendaryActions_label;
final ImageView legendaryActions_divider;
final LinearLayout lairActions;
final TextView lairActions_label;
final ImageView lairActions_divider;
final LinearLayout regionalEffects;
final TextView regionalEffects_label;
final ImageView regionalEffects_divider;
ViewHolder(@NonNull View root) {
this.root = root;
name = root.findViewById(R.id.name);
meta = root.findViewById(R.id.meta);
armorClass = root.findViewById(R.id.armorClass);
hitPoints = root.findViewById(R.id.hitPoints);
speed = root.findViewById(R.id.speed);
strength = root.findViewById(R.id.strength);
dexterity = root.findViewById(R.id.dexterity);
constitution = root.findViewById(R.id.constitution);
intelligence = root.findViewById(R.id.intelligence);
wisdom = root.findViewById(R.id.wisdom);
charisma = root.findViewById(R.id.charisma);
savingThrows = root.findViewById(R.id.savingThrows);
skills = root.findViewById(R.id.skills);
damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities);
damageResistances = root.findViewById(R.id.damageResistances);
damageImmunities = root.findViewById(R.id.damageImmunities);
conditionImmunities = root.findViewById(R.id.conditionImmunities);
senses = root.findViewById(R.id.senses);
languages = root.findViewById(R.id.languages);
challenge = root.findViewById(R.id.challenge);
abilities = root.findViewById(R.id.abilities);
actions = root.findViewById(R.id.actions);
actions_divider = root.findViewById(R.id.actions_divider);
actions_label = root.findViewById(R.id.actions_label);
reactions = root.findViewById(R.id.reactions);
reactions_divider = root.findViewById(R.id.reactions_divider);
reactions_label = root.findViewById(R.id.reactions_label);
legendaryActions = root.findViewById(R.id.legendaryActions);
legendaryActions_divider = root.findViewById(R.id.legendaryActions_divider);
legendaryActions_label = root.findViewById(R.id.legendaryActions_label);
lairActions = root.findViewById(R.id.lairActions);
lairActions_divider = root.findViewById(R.id.lairActions_divider);
lairActions_label = root.findViewById(R.id.lairActions_label);
regionalEffects = root.findViewById(R.id.regionalEffects);
regionalEffects_divider = root.findViewById(R.id.regionalEffects_divider);
regionalEffects_label = root.findViewById(R.id.regionalEffects_label);
}
}
}

View File

@@ -0,0 +1,217 @@
package com.majinnaibu.monstercards.ui.monster;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.majinnaibu.monstercards.models.Monster;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MonsterImportViewModel extends ViewModel {
private final MutableLiveData<List<String>> mAbilities;
private final MutableLiveData<List<String>> mActions;
private final MutableLiveData<String> mArmorClass;
private final MutableLiveData<String> mChallenge;
private final MutableLiveData<String> mCharisma;
private final MutableLiveData<String> mConditionImmunities;
private final MutableLiveData<String> mConstitution;
private final MutableLiveData<String> mDamageResistances;
private final MutableLiveData<String> mDamageImmunities;
private final MutableLiveData<String> mDamageVulnerabilities;
private final MutableLiveData<String> mDexterity;
private final MutableLiveData<String> mHitPoints;
private final MutableLiveData<String> mIntelligence;
private final MutableLiveData<List<String>> mLairActions;
private final MutableLiveData<String> mLanguages;
private final MutableLiveData<List<String>> mLegendaryActions;
private final MutableLiveData<String> mMeta;
private final MutableLiveData<String> mName;
private final MutableLiveData<List<String>> mReactions;
private final MutableLiveData<List<String>> mRegionalEffects;
private final MutableLiveData<String> mSavingThrows;
private final MutableLiveData<String> mSenses;
private final MutableLiveData<String> mSkills;
private final MutableLiveData<String> mSpeed;
private final MutableLiveData<String> mStrength;
private final MutableLiveData<String> mWisdom;
private final MutableLiveData<UUID> mMonsterId;
private Monster mMonster;
public MonsterImportViewModel() {
mMonster = null;
mAbilities = new MutableLiveData<>(new ArrayList<>());
mActions = new MutableLiveData<>(new ArrayList<>());
mArmorClass = new MutableLiveData<>("");
mChallenge = new MutableLiveData<>("");
mCharisma = new MutableLiveData<>("");
mConditionImmunities = new MutableLiveData<>("");
mConstitution = new MutableLiveData<>("");
mDamageImmunities = new MutableLiveData<>("");
mDamageResistances = new MutableLiveData<>("");
mDamageVulnerabilities = new MutableLiveData<>("");
mDexterity = new MutableLiveData<>("");
mHitPoints = new MutableLiveData<>("");
mIntelligence = new MutableLiveData<>("");
mLairActions = new MutableLiveData<>(new ArrayList<>());
mLanguages = new MutableLiveData<>("");
mLegendaryActions = new MutableLiveData<>(new ArrayList<>());
mMeta = new MutableLiveData<>("");
mName = new MutableLiveData<>("");
mReactions = new MutableLiveData<>(new ArrayList<>());
mRegionalEffects = new MutableLiveData<>(new ArrayList<>());
mSavingThrows = new MutableLiveData<>("");
mSenses = new MutableLiveData<>("");
mSkills = new MutableLiveData<>("");
mSpeed = new MutableLiveData<>("");
mStrength = new MutableLiveData<>("");
mWisdom = new MutableLiveData<>("");
mMonsterId = new MutableLiveData<>(UUID.fromString("00000000-0000-0000-0000-000000000000"));
}
public LiveData<List<String>> getAbilities() {
return mAbilities;
}
public LiveData<List<String>> getActions() {
return mActions;
}
public LiveData<List<String>> getReactions() {
return mReactions;
}
public LiveData<List<String>> getLegendaryActions() {
return mLegendaryActions;
}
public LiveData<List<String>> getLairActions() {
return mLairActions;
}
public LiveData<List<String>> getRegionalEffects() {
return mRegionalEffects;
}
public LiveData<String> getArmorClass() {
return mArmorClass;
}
public LiveData<String> getChallenge() {
return mChallenge;
}
public LiveData<String> getCharisma() {
return mCharisma;
}
public LiveData<String> getConditionImmunities() {
return mConditionImmunities;
}
public LiveData<String> getConstitution() {
return mConstitution;
}
public LiveData<String> getDamageResistances() {
return mDamageResistances;
}
public LiveData<String> getDamageImmunities() {
return mDamageImmunities;
}
public LiveData<String> getDamageVulnerabilities() {
return mDamageVulnerabilities;
}
public LiveData<String> getDexterity() {
return mDexterity;
}
public LiveData<String> getHitPoints() {
return mHitPoints;
}
public LiveData<String> getIntelligence() {
return mIntelligence;
}
public LiveData<String> getLanguages() {
return mLanguages;
}
public LiveData<String> getMeta() {
return mMeta;
}
public LiveData<String> getName() {
return mName;
}
public LiveData<String> getSavingThrows() {
return mSavingThrows;
}
public LiveData<String> getSenses() {
return mSenses;
}
public LiveData<String> getSkills() {
return mSkills;
}
public LiveData<String> getSpeed() {
return mSpeed;
}
public LiveData<String> getStrength() {
return mStrength;
}
public LiveData<String> getWisdom() {
return mWisdom;
}
public LiveData<UUID> getId() {
return mMonsterId;
}
public Monster getMonster() {
return mMonster;
}
public void setMonster(@NonNull Monster monster) {
mMonster = monster;
mAbilities.setValue(mMonster.getAbilityDescriptions());
mActions.setValue(mMonster.getActionDescriptions());
mArmorClass.setValue(mMonster.getArmorClass());
mChallenge.setValue(mMonster.getChallengeRatingDescription());
mCharisma.setValue(monster.getCharismaDescription());
mConditionImmunities.setValue(mMonster.getConditionImmunitiesDescription());
mConstitution.setValue(monster.getConstitutionDescription());
mDamageImmunities.setValue(mMonster.getDamageImmunitiesDescription());
mDamageResistances.setValue(mMonster.getDamageResistancesDescription());
mDamageVulnerabilities.setValue(mMonster.getDamageVulnerabilitiesDescription());
mDexterity.setValue(monster.getDexterityDescription());
mHitPoints.setValue(mMonster.getHitPoints());
mIntelligence.setValue(monster.getIntelligenceDescription());
mLairActions.setValue(mMonster.getLairActionDescriptions());
mLanguages.setValue(mMonster.getLanguagesDescription());
mLegendaryActions.setValue(mMonster.getLegendaryActionDescriptions());
mMeta.setValue(mMonster.getMeta());
mMonsterId.setValue(mMonster.id);
mName.setValue(mMonster.name);
mReactions.setValue(monster.getReactionDescriptions());
mRegionalEffects.setValue(monster.getRegionalActionDescriptions());
mSavingThrows.setValue(monster.getSavingThrowsDescription());
mSenses.setValue(monster.getSensesDescription());
mSkills.setValue(monster.getSkillsDescription());
mSpeed.setValue(mMonster.getSpeedText());
mStrength.setValue(monster.getStrengthDescription());
mWisdom.setValue(monster.getWisdomDescription());
}
}

View File

@@ -0,0 +1,53 @@
package com.majinnaibu.monstercards.ui.search;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.ui.shared.MCFragment;
public class SearchFragment extends MCFragment {
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_search, container, false);
MonsterRepository repository = this.getMonsterRepository();
SearchResultsRecyclerViewAdapter adapter = new SearchResultsRecyclerViewAdapter(repository, null);
final RecyclerView recyclerView = root.findViewById(R.id.monster_list);
assert recyclerView != null;
setupRecyclerView(recyclerView, adapter);
final TextView textView = root.findViewById(R.id.search_query);
textView.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
adapter.doSearch(textView.getText().toString());
}
});
return root;
}
private void setupRecyclerView(@NonNull RecyclerView recyclerView, @NonNull SearchResultsRecyclerViewAdapter adapter) {
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
}

View File

@@ -0,0 +1,90 @@
package com.majinnaibu.monstercards.ui.search;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.utils.Logger;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
public class SearchResultsRecyclerViewAdapter extends RecyclerView.Adapter<SearchResultsRecyclerViewAdapter.ViewHolder> {
private final MonsterRepository mRepository;
private final ItemCallback mOnClickHandler;
private String mSearchText;
private List<Monster> mValues;
private Disposable mSubscriptionHandler;
public SearchResultsRecyclerViewAdapter(MonsterRepository repository,
ItemCallback onClick) {
mRepository = repository;
mSearchText = "";
mValues = new ArrayList<>();
mOnClickHandler = onClick;
mSubscriptionHandler = null;
doSearch(mSearchText);
}
public void doSearch(String searchText) {
if (mSubscriptionHandler != null && !mSubscriptionHandler.isDisposed()) {
mSubscriptionHandler.dispose();
}
mSearchText = searchText;
Flowable<List<Monster>> foundMonsters = mRepository.searchMonsters(mSearchText);
mSubscriptionHandler = foundMonsters.subscribe(monsters -> {
mValues = monsters;
notifyDataSetChanged();
},
throwable -> Logger.logError("Error performing search", throwable));
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.monster_list_content, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
Monster monster = mValues.get(position);
holder.mContentView.setText(monster.name);
holder.itemView.setTag(monster);
holder.itemView.setOnClickListener(view -> {
if (mOnClickHandler != null) {
mOnClickHandler.onItem(monster);
}
});
}
@Override
public int getItemCount() {
return mValues.size();
}
public interface ItemCallback {
void onItem(Monster monster);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
final TextView mContentView;
ViewHolder(View view) {
super(view);
mContentView = view.findViewById(R.id.content);
}
}
}

View File

@@ -0,0 +1,25 @@
package com.majinnaibu.monstercards.ui.shared;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class ChangeTrackedViewModel extends ViewModel {
private final MutableLiveData<Boolean> mHasChanges;
public ChangeTrackedViewModel() {
mHasChanges = new MutableLiveData<>(false);
}
public boolean hasChanges() {
Boolean value = mHasChanges.getValue();
return value != null && value;
}
protected void makeDirty() {
mHasChanges.setValue(true);
}
protected void makeClean() {
mHasChanges.setValue(false);
}
}

View File

@@ -0,0 +1,35 @@
package com.majinnaibu.monstercards.ui.shared;
import android.app.Activity;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.majinnaibu.monstercards.MonsterCardsApplication;
import com.majinnaibu.monstercards.data.MonsterRepository;
public class MCFragment extends Fragment {
public MonsterCardsApplication getApplication() {
return (MonsterCardsApplication) requireActivity().getApplication();
}
protected MonsterRepository getMonsterRepository() {
return this.getApplication().getMonsterRepository();
}
public AppCompatActivity requireAppCompatActivity() {
return (AppCompatActivity) requireActivity();
}
public void setTitle(CharSequence title) {
Activity activity = requireActivity();
if (activity instanceof AppCompatActivity) {
AppCompatActivity appCompatActivity = (AppCompatActivity) activity;
ActionBar supportActionBar = appCompatActivity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setTitle(title);
}
}
}
}

View File

@@ -0,0 +1,98 @@
package com.majinnaibu.monstercards.ui.shared;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
public class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback {
private final Drawable icon;
private final ColorDrawable background;
private final Paint clearPaint;
private final OnSwipeCallback mOnDelete;
private final OnMoveCallback mOnMove;
private final Context mContext;
public SwipeToDeleteCallback(@NonNull Context context, OnSwipeCallback onDelete, OnMoveCallback onMove) {
super(onMove == null ? 0 : ItemTouchHelper.UP | ItemTouchHelper.DOWN, onDelete == null ? 0 : ItemTouchHelper.LEFT);
mOnDelete = onDelete;
mOnMove = onMove;
mContext = context;
icon = ContextCompat.getDrawable(mContext, R.drawable.ic_delete_white_36);
background = new ColorDrawable(context.getResources().getColor(R.color.red));
clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
@Override
public boolean onMove(
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target
) {
if (mOnMove != null) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
return mOnMove.onMove(from, to);
}
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
if (mOnDelete != null) {
int position = viewHolder.getAdapterPosition();
mOnDelete.onSwipe(position, direction);
}
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
View itemView = viewHolder.itemView;
int itemHeight = itemView.getBottom() - itemView.getTop();
boolean isCancelled = dX == 0 && !isCurrentlyActive;
if (isCancelled) {
c.drawRect(itemView.getRight() + dX, itemView.getTop(), itemView.getRight(), itemView.getBottom(), clearPaint);
return;
}
// Draw the red delete background
background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom());
background.draw(c);
// Calculate position of delete icon
int iconHeight = icon.getIntrinsicHeight();
int iconWidth = icon.getIntrinsicWidth();
int iconTop = itemView.getTop() + (itemHeight - iconHeight) / 2;
int iconMargin = (itemHeight - iconHeight) / 2;
int iconLeft = itemView.getRight() - iconMargin - iconWidth;
int iconRight = itemView.getRight() - iconMargin;
int iconBottom = iconTop + iconHeight;
// Draw the icon
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom);
icon.draw(c);
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
public interface OnSwipeCallback {
void onSwipe(int position, int direction);
}
public interface OnMoveCallback {
boolean onMove(int from, int to);
}
}

View File

@@ -0,0 +1,65 @@
package com.majinnaibu.monstercards.utils;
import androidx.lifecycle.MutableLiveData;
import java.util.Objects;
@SuppressWarnings("unused")
public class ChangeTrackedLiveData<T> extends MutableLiveData<T> {
private final OnValueChangedCallback<T> mOnValueChangedCallback;
private final OnValueDirtiedCallback mOnValueDirtiedCallback;
private T mReferenceValue;
public ChangeTrackedLiveData(T initialValue, OnValueChangedCallback<T> onValueChanged, OnValueDirtiedCallback onValueDirtied) {
super(initialValue);
mReferenceValue = initialValue;
mOnValueChangedCallback = onValueChanged;
if (mOnValueChangedCallback != null) {
mOnValueChangedCallback.onValueChanged(initialValue);
}
mOnValueDirtiedCallback = onValueDirtied;
}
public ChangeTrackedLiveData(T initialValue, OnValueChangedCallback<T> callback) {
this(initialValue, callback, null);
}
public ChangeTrackedLiveData(T initialValue, OnValueDirtiedCallback callback) {
this(initialValue, null, callback);
}
public void setReferenceValue(T referenceValue) {
mReferenceValue = referenceValue;
}
public void setCurrentValueAsReference() {
mReferenceValue = getValue();
}
public void resetValue(T value) {
mReferenceValue = value;
setValue(value);
}
@Override
public void setValue(T value) {
if (!Objects.equals(getValue(), value)) {
super.setValue(value);
if (mOnValueChangedCallback != null) {
mOnValueChangedCallback.onValueChanged(value);
}
if (!Objects.equals(mReferenceValue, value) && mOnValueDirtiedCallback != null) {
mOnValueDirtiedCallback.onValueDirtied();
}
}
}
public interface OnValueDirtiedCallback {
void onValueDirtied();
}
public interface OnValueChangedCallback<T> {
void onValueChanged(T value);
}
}

View File

@@ -0,0 +1,139 @@
package com.majinnaibu.monstercards.utils;
import android.util.Log;
@SuppressWarnings("unused")
public class Logger {
public static final String LOG_TAG = "MonsterCards";
public static void logUnimplementedMethod() {
Exception ex = new Exception();
StackTraceElement[] stackTrace = ex.getStackTrace();
String location = stackTrace[1].getClassName() + "." + stackTrace[1].getMethodName() + ":" + stackTrace[1].getLineNumber();
logDebug("Method not yet implemented " + location);
}
public static void logUnhandledError(Throwable e) {
StackTraceElement stackTraceElement = e.getStackTrace()[0];
String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber();
logDebug("Exception was caught but not properly handled " + location);
}
public static void logUnimplementedFeature(String featureDescription) {
Exception ex = new Exception();
StackTraceElement[] stackTrace = ex.getStackTrace();
String location = stackTrace[1].getClassName() + "." + stackTrace[1].getMethodName() + ":" + stackTrace[1].getLineNumber();
logDebug("Feature not yet implemented " + featureDescription + " at " + location);
}
//region WTF
public static void logWTF(String message) {
Log.wtf(LOG_TAG, message);
}
public static void logWTF(Throwable throwable) {
StackTraceElement stackTraceElement = throwable.getStackTrace()[0];
String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber();
String message = String.format("Unexpected error occurred at %s.", location);
Log.wtf(LOG_TAG, message, throwable);
}
public static void logWTF(String message, Throwable throwable) {
Log.wtf(LOG_TAG, message, throwable);
}
//endregion
//region Error
public static void logError(String message) {
Log.e(LOG_TAG, message);
}
public static void logError(Throwable throwable) {
StackTraceElement stackTraceElement = throwable.getStackTrace()[0];
String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber();
String message = String.format("Unexpected error occurred at %s.", location);
Log.e(LOG_TAG, message, throwable);
}
public static void logError(String message, Throwable throwable) {
Log.e(LOG_TAG, message, throwable);
}
//endregion
//region Warning
public static void logWarning(String message) {
Log.w(LOG_TAG, message);
}
public static void logWarning(Throwable throwable) {
StackTraceElement stackTraceElement = throwable.getStackTrace()[0];
String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber();
String message = String.format("Unexpected error occurred at %s.", location);
Log.w(LOG_TAG, message, throwable);
}
public static void logWarning(String message, Throwable throwable) {
Log.w(LOG_TAG, message, throwable);
}
//endregion
//region Info
public static void logInfo(String message) {
Log.i(LOG_TAG, message);
}
public static void logInfo(Throwable throwable) {
StackTraceElement stackTraceElement = throwable.getStackTrace()[0];
String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber();
String message = String.format("Unexpected error occurred at %s.", location);
Log.i(LOG_TAG, message, throwable);
}
public static void logInfo(String message, Throwable throwable) {
Log.i(LOG_TAG, message, throwable);
}
//endregion
//region Debug
public static void logDebug(String message) {
Log.d(LOG_TAG, message);
}
public static void logDebug(Throwable throwable) {
StackTraceElement stackTraceElement = throwable.getStackTrace()[0];
String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber();
String message = String.format("Unexpected error occurred at %s.", location);
Log.d(LOG_TAG, message, throwable);
}
public static void logDebug(String message, Throwable throwable) {
Log.d(LOG_TAG, message, throwable);
}
//endregion
//region Verbose
public static void logVerbose(String message) {
Log.v(LOG_TAG, message);
}
public static void logVerbose(Throwable throwable) {
StackTraceElement stackTraceElement = throwable.getStackTrace()[0];
String location = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber();
String message = String.format("Unexpected error occurred at %s.", location);
Log.v(LOG_TAG, message, throwable);
}
public static void logVerbose(String message, Throwable throwable) {
Log.v(LOG_TAG, message, throwable);
}
//endregion
}

View File

@@ -0,0 +1,75 @@
package com.majinnaibu.monstercards.utils;
import android.text.Editable;
import android.text.TextWatcher;
@SuppressWarnings("unused")
public class TextChangedListener implements TextWatcher {
private final BeforeTextChangedCallback mBeforeTextChangedCallback;
private final OnTextChangedCallback mOnTextChangedCallback;
private final AfterTextChangedCallback mAfterTextChangedCallback;
public TextChangedListener(BeforeTextChangedCallback beforeTextChangedCallback, OnTextChangedCallback onTextChangedCallback, AfterTextChangedCallback afterTextChangedCallback) {
mBeforeTextChangedCallback = beforeTextChangedCallback;
mOnTextChangedCallback = onTextChangedCallback;
mAfterTextChangedCallback = afterTextChangedCallback;
}
public TextChangedListener(OnTextChangedCallback callback) {
this(null, callback, null);
}
public TextChangedListener(BeforeTextChangedCallback callback) {
this(callback, null, null);
}
public TextChangedListener(AfterTextChangedCallback callback) {
this(null, null, callback);
}
public TextChangedListener(BeforeTextChangedCallback beforeTextChangedCallback, OnTextChangedCallback onTextChangedCallback) {
this(beforeTextChangedCallback, onTextChangedCallback, null);
}
public TextChangedListener(BeforeTextChangedCallback beforeTextChangedCallback, AfterTextChangedCallback afterTextChangedCallback) {
this(beforeTextChangedCallback, null, afterTextChangedCallback);
}
public TextChangedListener(OnTextChangedCallback onTextChangedCallback, AfterTextChangedCallback afterTextChangedCallback) {
this(null, onTextChangedCallback, afterTextChangedCallback);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (mBeforeTextChangedCallback != null) {
mBeforeTextChangedCallback.beforeTextChanged(s, start, count, after);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mOnTextChangedCallback != null) {
mOnTextChangedCallback.onTextChanged(s, start, before, count);
}
}
@Override
public void afterTextChanged(Editable s) {
if (mAfterTextChangedCallback != null) {
mAfterTextChangedCallback.afterTextChanged(s);
}
}
public interface BeforeTextChangedCallback {
void beforeTextChanged(CharSequence s, int start, int count, int after);
}
public interface OnTextChangedCallback {
void onTextChanged(CharSequence s, int start, int before, int count);
}
public interface AfterTextChangedCallback {
void afterTextChanged(Editable s);
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/colorOnPrimary" android:state_checked="true"/>
<item android:color="@color/colorPrimary" android:state_checked="false"/>
</selector>

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