Compare commits

..

448 Commits

Author SHA1 Message Date
e3b6196349 Fixes the xcode build and adds stubs for missing classes. 2026-09-23 19:47:53 -07:00
baed51259e Remove log for expected RxJava InterruptedException 2026-09-22 21:58:09 -07:00
ba77d1b787 Configure RxJava Global Error Handler to consume Unhandled thread interruptions harmlessly 2026-09-22 21:58:09 -07:00
85aafd8a02 RxJava gracefully handles intercepted InterruptedException instead of crashing 2026-09-22 21:58:09 -07:00
71a2146c4b Gracefully intercept networking interruption errors 2026-09-22 21:58:09 -07:00
509ac11749 Throw and safely catch InterruptedException on task cancellation 2026-09-22 21:58:09 -07:00
5ee7f02b42 Catch exceptions and handle graceful zip closes when cancelled 2026-09-22 21:58:09 -07:00
a7f1ff712a Fixes imports. 2026-09-22 21:58:09 -07:00
2385f1d088 Add extensive logging to Git Repo Extractor to trace missing files 2026-09-22 21:58:09 -07:00
c247c2d998 Fix github archive directory prefix structure logic 2026-09-22 21:58:09 -07:00
35f45513aa Initialize ID for PF2e imported monsters to prevent SQLite exceptions 2026-09-22 21:58:09 -07:00
a7b0e68197 Implemented PF2e parsing logic 2026-09-22 21:58:09 -07:00
f74e21e7d9 Unified Import Source configuration, added concurrency prevention and combined UI 2026-09-22 21:58:09 -07:00
cb26cec7bc Added Github Repo Import Worker and Framework 2026-09-22 21:58:09 -07:00
8249c68266 Added I/O try-catch and toasts 2026-09-22 21:58:09 -07:00
338a7e4c0e Adds option to import from Open5e.com 2026-09-22 21:58:09 -07:00
d099d878a0 Adds one central import form. 2026-09-22 21:58:09 -07:00
339e413f82 Updates dependencies to latest versions. 2026-09-22 21:58:09 -07:00
e888bbf8a8 t_a6ad7840: build-verification evidence — Debug+Release BUILD SUCCEEDED, app runs on simulator 2026-09-22 18:41:53 -07:00
a5bf6639e8 Updates ios targets to 17.0. 2026-09-22 18:41:53 -07:00
9dd5c7e627 Add EntityImporter protocol (from t_a150f356) 2026-09-22 16:23:49 -07:00
9a38c6d98f Add BinderExporter iOS implementation with tests 2026-09-22 16:22:55 -07:00
8f1689574c Adds .build to gitignore. 2026-09-22 16:00:29 -07:00
9c1e27d473 Document build/test verification for t_b72560e5
Records what was verified without the full xcodebuild pipeline (valid pbxproj,
correct 3-package SPM graph, MarkdownUI 2.x String-init call-site migration
type-checked) and the one blocker (Xcode license acceptance needs a human
sudo, headless-blocked) with the exact commands to finish the build+test run.
No code regressions found in the version bumps.
2026-09-22 16:00:29 -07:00
bd933ca671 t_a6ad7840: Update third-party dependencies to latest compatible versions
Migrate MarkdownUI 0.5.1 -> 2.4.1 (upToNextMajorVersion). This single
direct-upgrade collapses the 7-package resolved graph to 3 packages,
dropping four archived/retired transitive dependencies:

  Dropped (was pulled in by MarkdownUI 0.5.1):
    - AttributedText 0.3.0        (archived 2023-01)
    - SwiftCommonMark 0.1.1       (archived 2023-01)
    - combine-schedulers 0.4.1    (Point-Free; iOS 16+ requirement)
    - xctest-dynamic-overlay 0.1.0 (unused in source; superseded by
                                    swift-issue-reporting; no longer
                                    needed as MarkdownUI test-only)
    - SwiftDocOrg/swift-cmark 0.29.0 (archived; replaced by
                                    swiftlang/swift-cmark 0.8.0)

  Pinned (MarkdownUI 2.4.1 transitive graph):
    - MarkdownUI 2.4.1            (gonzalezreal/MarkdownUI @ 5f61335)
    - NetworkImage 6.0.1          (gonzalezreal/NetworkImage @ 2849f53)
      replaces NetworkImage 2.1.0 (iOS 14 baseline, compatible with
      the iOS 15.0 target set by t_fe859d8e)
    - swift-cmark 0.8.0           (swiftlang/swift-cmark @ 924936d)

Call-site migration (MarkdownUI 0.5.1 -> 2.x):
  MarkdownUI 2.x replaced 'Markdown(Document(_:))' (a CommonMark
  Document) with 'Markdown(_ string:)' / 'Markdown(_ content:
  MarkdownContent)'. Two call-sites in MonsterDetailView.swift
  updated to pass the rendered Markdown string directly.

Package.resolved regenerated against the resolved graph (version 2
format). Verified clean SPM resolution and that the call-site
pattern compiles against MarkdownUI 2.4.1 on swift 6.3.3.

No changes to Xcode project-level settings (delegated to t_fe859d8e).
2026-09-22 16:00:29 -07:00
70d549f929 t_fe859d8e: Xcode project settings migration — objectVersion 52→58, SWIFT_LANGUAGE_VERSION=6, IPHONEOS_DEPLOYMENT_TARGET=15.0, upgrade/swift-checks→2700 2026-09-22 16:00:29 -07:00
c674e2c27a Add BinderImporter + import models + unit tests
- BinderImportModels.swift: Codable data models for Binder (TRPG platform) card format. Matches Android Monster.java field names.
- BinderImporter.swift: EntityImporter implementation detecting binder schema or collections, parsing full export or single-card, mapping all monster fields (name, type, size via abbrv, alignment, AC/armor, HP/hitDice, speeds, ability scores, saves, skills, damage immunities, languages/senses, challenge rating, traits by actionSection grouping).
- MonsterImportHelper.fromJSON: Updated to register BinderImporter.
- BinderImporterTest.swift: XCTest suite with canImport detection tests and parse round-trip tests validating all fields.

Acceptance criteria:
1. ✅ Conforms to EntityImporter protocol
2. ✅ Unit test validates field roundtrips
3. ✅ MonsterImportHelper.fromJSON includes BinderImporter entry
4. ✅ No Android imports present (iOS-native only)
5. ✅ Follows naming/style conventions from DnDBeyondImporter and MonsterJsonImporter
2026-09-22 16:00:29 -07:00
a3998d6c1d Cleans up import and export options. 2026-09-20 19:21:00 -07:00
77176a6d4f Adds more ways to add monsters. 2026-09-20 17:20:43 -07:00
fa25c1bef3 Adds some example .monster files. 2026-09-20 17:20:26 -07:00
0e21554773 Removes Clear Dashboard and Remove Monster from the fab on the dashboard.. 2026-09-20 16:32:55 -07:00
0cbd03da69 Adds removing a monster from the dashboard via long press. 2026-09-20 16:29:13 -07:00
c06b53ed6b Adds icon/logo images and a splash screen. 2026-09-20 15:12:42 -07:00
2d043320e0 Adds .ide ignore rules and run configurations. 2026-09-20 13:51:26 -07:00
1e0c9ece0f Fixed status bar color. 2026-09-20 13:43:53 -07:00
65d1b64e0b Adds some project docs and images. 2026-09-20 03:35:23 -07:00
ddb58622d6 Makes monster editor smoother and fixes colors. 2026-09-16 22:04:30 -07:00
d63b125175 Adds edge to edge rendering. 2026-09-16 19:29:37 -07:00
b66b653982 Adds card and binder export and import. 2026-09-16 19:25:32 -07:00
ccb06960a2 Makes monsters shareable. 2026-09-08 13:58:35 -07:00
ccc9fadef9 Makes importing a monster navigate to it in the library. 2026-09-07 22:21:45 -07:00
a3dace36de Adds import support from our other project. 2026-09-07 21:57:59 -07:00
a3d1a139e9 Adds ability to import from a public dndbeyond character. 2026-09-07 20:34:12 -07:00
2af18cbf1d Updates monster imports from tetra cube. 2026-09-07 20:10:36 -07:00
044202cb3a Refactors monster importers to a common interface. 2026-09-07 18:16:08 -07:00
b3ac787360 Makes .monster files open with MonsterCards. 2026-09-07 18:06:02 -07:00
60a26ba81d Adds plan to support other import / export formats on android. 2026-09-07 17:03:53 -07:00
edc6415ad5 Makes the dashboard editable. 2026-09-07 15:44:05 -07:00
e2a1fafaa9 Adds collections to search and disambiguates results. 2026-09-07 15:21:19 -07:00
51b0245cfe Adds monster collections. 2026-09-07 15:14:23 -07:00
605f4b99f7 Removes Flipper and SOLoader as dependencies. 2026-09-07 14:05:53 -07:00
92b619a407 Updates android jdk version from 17 to 21. 2026-08-14 18:00:34 -07:00
3a775f47e0 Updates AGP to 9.1.0, Gradle to 9.5.0, and JDK to 21. 2026-08-14 17:56:09 -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
609 changed files with 63173 additions and 839 deletions

10
.gitignore vendored Normal file
View File

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

18
.idea/.gitignore generated vendored Normal file
View File

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

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
MonsterCards

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

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

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

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

18
.idea/gradle.xml generated Normal file
View File

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

60
.idea/misc.xml generated Normal file
View File

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

17
.idea/runConfigurations.xml generated Normal file
View File

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

68
.idea/runConfigurations/app.xml generated Normal file
View File

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

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

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

15
Android/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# Build outputs
build/
app/build/
.externalNativeBuild/
.cxx/
captures/
local.properties
*.iml
# Android / Gradle caches
.gradle/
.artifacts/
# System
.DS_Store

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

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

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

@@ -0,0 +1 @@
MonsterCards

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

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

23
Android/.idea/appInsightsSettings.xml generated Normal file
View File

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

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="21" />
</component>
</project>

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

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

10
Android/.idea/migrations.xml generated Normal file
View File

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

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

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

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

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

68
Android/.idea/runConfigurations/app.xml generated Normal file
View File

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

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>

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 {
namespace "com.majinnaibu.monstercards"
compileSdk 37
defaultConfig {
applicationId "com.majinnaibu.monstercards"
minSdkVersion 24
//noinspection OldTargetApi
targetSdk 36
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_21
targetCompatibility JavaVersion.VERSION_21
}
buildFeatures {
viewBinding true
buildConfig true
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
// Included libs
implementation fileTree(dir: "libs", include: ["*.jar"])
// Google
implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'com.google.android.material:material:1.14.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.2'
implementation "androidx.navigation:navigation-fragment:2.10.1"
implementation "androidx.navigation:navigation-ui:2.10.1"
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.11.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0'
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
// Room DB
implementation "io.reactivex.rxjava3:rxjava:3.1.12"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
implementation "androidx.room:room-runtime:2.8.5"
annotationProcessor "androidx.room:room-compiler:2.8.5"
implementation "androidx.room:room-rxjava3:2.8.5"
//testImplementation "androidx.room:room-testing:2.8.5"
// AppCenter
debugImplementation "com.microsoft.appcenter:appcenter-analytics:5.0.6"
debugImplementation "com.microsoft.appcenter:appcenter-crashes:5.0.6"
// Other 3rd Party
implementation 'com.atlassian.commonmark:commonmark:0.17.0'
implementation "com.google.code.gson:gson:2.14.0"
}

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

View File

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

View File

@@ -0,0 +1,663 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "0ab331e246de46304f847d7ed2d10124",
"entities": [
{
"tableName": "monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "strengthScore",
"columnName": "strength_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "strengthSavingThrowAdvantage",
"columnName": "strength_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "shieldBonus",
"columnName": "shield_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "naturalArmorBonus",
"columnName": "natural_armor_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "otherArmorDescription",
"columnName": "other_armor_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "hitDice",
"columnName": "hit_dice",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "hasCustomHP",
"columnName": "has_custom_hit_points",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "customHPDescription",
"columnName": "custom_hit_points_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "walkSpeed",
"columnName": "walk_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "burrowSpeed",
"columnName": "burrow_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "climbSpeed",
"columnName": "climb_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "flySpeed",
"columnName": "fly_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "canHover",
"columnName": "can_hover",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "swimSpeed",
"columnName": "swim_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hasCustomSpeed",
"columnName": "has_custom_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "customSpeedDescription",
"columnName": "custom_speed_description",
"affinity": "TEXT"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "customProficiencyBonus",
"columnName": "custom_proficiency_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "telepathyRange",
"columnName": "telepathy_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "understandsButDescription",
"columnName": "understands_but_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "sourceUrl",
"columnName": "source_url",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "bonusActions",
"columnName": "bonus_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "mythicActions",
"columnName": "mythic_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActionsDescription",
"columnName": "legendary_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsDescription",
"columnName": "lair_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsEndNote",
"columnName": "lair_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsDescription",
"columnName": "regional_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsEndNote",
"columnName": "regional_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "mythicActionsDescription",
"columnName": "mythic_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "monsters_fts",
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"ftsVersion": "FTS4",
"ftsOptions": {
"tokenizer": "simple",
"tokenizerArgs": [],
"contentTable": "monsters",
"languageIdColumnName": "",
"matchInfo": "FTS4",
"notIndexedColumns": [],
"prefixSizes": [],
"preferredOrder": "ASC"
},
"contentSyncTriggers": [
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
]
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "dashboard_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_dashboard_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0ab331e246de46304f847d7ed2d10124')"
]
}
}

View File

@@ -0,0 +1,768 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "38f6bbd6f1e298b20e17b3a08fbc7f70",
"entities": [
{
"tableName": "monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "strengthScore",
"columnName": "strength_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "strengthSavingThrowAdvantage",
"columnName": "strength_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "shieldBonus",
"columnName": "shield_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "naturalArmorBonus",
"columnName": "natural_armor_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "otherArmorDescription",
"columnName": "other_armor_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "hitDice",
"columnName": "hit_dice",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "hasCustomHP",
"columnName": "has_custom_hit_points",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "customHPDescription",
"columnName": "custom_hit_points_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "walkSpeed",
"columnName": "walk_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "burrowSpeed",
"columnName": "burrow_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "climbSpeed",
"columnName": "climb_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "flySpeed",
"columnName": "fly_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "canHover",
"columnName": "can_hover",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "swimSpeed",
"columnName": "swim_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hasCustomSpeed",
"columnName": "has_custom_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "customSpeedDescription",
"columnName": "custom_speed_description",
"affinity": "TEXT"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "customProficiencyBonus",
"columnName": "custom_proficiency_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "telepathyRange",
"columnName": "telepathy_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "understandsButDescription",
"columnName": "understands_but_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "sourceUrl",
"columnName": "source_url",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "bonusActions",
"columnName": "bonus_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "mythicActions",
"columnName": "mythic_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActionsDescription",
"columnName": "legendary_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsDescription",
"columnName": "lair_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsEndNote",
"columnName": "lair_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsDescription",
"columnName": "regional_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsEndNote",
"columnName": "regional_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "mythicActionsDescription",
"columnName": "mythic_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "playerName",
"columnName": "player_name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "background",
"columnName": "background",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "personalityTraits",
"columnName": "personality_traits",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "ideals",
"columnName": "ideals",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "bonds",
"columnName": "bonds",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "flaws",
"columnName": "flaws",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "age",
"columnName": "age",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "eyes",
"columnName": "eyes",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "skin",
"columnName": "skin",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "hair",
"columnName": "hair",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "appearance",
"columnName": "appearance",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "backstory",
"columnName": "backstory",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alliesAndOrganizations",
"columnName": "allies_and_organizations",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "monsters_fts",
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"ftsVersion": "FTS4",
"ftsOptions": {
"tokenizer": "simple",
"tokenizerArgs": [],
"contentTable": "monsters",
"languageIdColumnName": "",
"matchInfo": "FTS4",
"notIndexedColumns": [],
"prefixSizes": [],
"preferredOrder": "ASC"
},
"contentSyncTriggers": [
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
]
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "dashboard_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_dashboard_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '38f6bbd6f1e298b20e17b3a08fbc7f70')"
]
}
}

View File

@@ -0,0 +1,768 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "38f6bbd6f1e298b20e17b3a08fbc7f70",
"entities": [
{
"tableName": "monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', `source_url` TEXT DEFAULT '', `bonus_actions` TEXT DEFAULT '[]', `mythic_actions` TEXT DEFAULT '[]', `legendary_actions_description` TEXT DEFAULT '', `lair_actions_description` TEXT DEFAULT '', `lair_actions_end_note` TEXT DEFAULT '', `regional_actions_description` TEXT DEFAULT '', `regional_actions_end_note` TEXT DEFAULT '', `mythic_actions_description` TEXT DEFAULT '', `player_name` TEXT NOT NULL DEFAULT '', `background` TEXT NOT NULL DEFAULT '', `personality_traits` TEXT NOT NULL DEFAULT '', `ideals` TEXT NOT NULL DEFAULT '', `bonds` TEXT NOT NULL DEFAULT '', `flaws` TEXT NOT NULL DEFAULT '', `age` TEXT NOT NULL DEFAULT '', `height` TEXT NOT NULL DEFAULT '', `weight` TEXT NOT NULL DEFAULT '', `eyes` TEXT NOT NULL DEFAULT '', `skin` TEXT NOT NULL DEFAULT '', `hair` TEXT NOT NULL DEFAULT '', `appearance` TEXT NOT NULL DEFAULT '', `backstory` TEXT NOT NULL DEFAULT '', `allies_and_organizations` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "strengthScore",
"columnName": "strength_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "strengthSavingThrowAdvantage",
"columnName": "strength_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "strengthSavingThrowProficiency",
"columnName": "strength_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexterityScore",
"columnName": "dexterity_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "dexteritySavingThrowAdvantage",
"columnName": "dexterity_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "dexteritySavingThrowProficiency",
"columnName": "dexterity_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionScore",
"columnName": "constitution_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "constitutionSavingThrowAdvantage",
"columnName": "constitution_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "constitutionSavingThrowProficiency",
"columnName": "constitution_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceScore",
"columnName": "intelligence_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "intelligenceSavingThrowAdvantage",
"columnName": "intelligence_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "intelligenceSavingThrowProficiency",
"columnName": "intelligence_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomScore",
"columnName": "wisdom_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "wisdomSavingThrowAdvantage",
"columnName": "wisdom_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "wisdomSavingThrowProficiency",
"columnName": "wisdom_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaScore",
"columnName": "charisma_score",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "charismaSavingThrowAdvantage",
"columnName": "charisma_saving_throw_advantage",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "charismaSavingThrowProficiency",
"columnName": "charisma_saving_throw_proficiency",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "armorType",
"columnName": "armor_type",
"affinity": "TEXT",
"defaultValue": "'none'"
},
{
"fieldPath": "shieldBonus",
"columnName": "shield_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "naturalArmorBonus",
"columnName": "natural_armor_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "otherArmorDescription",
"columnName": "other_armor_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "hitDice",
"columnName": "hit_dice",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "hasCustomHP",
"columnName": "has_custom_hit_points",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "customHPDescription",
"columnName": "custom_hit_points_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "walkSpeed",
"columnName": "walk_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "burrowSpeed",
"columnName": "burrow_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "climbSpeed",
"columnName": "climb_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "flySpeed",
"columnName": "fly_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "canHover",
"columnName": "can_hover",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "swimSpeed",
"columnName": "swim_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hasCustomSpeed",
"columnName": "has_custom_speed",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "customSpeedDescription",
"columnName": "custom_speed_description",
"affinity": "TEXT"
},
{
"fieldPath": "challengeRating",
"columnName": "challenge_rating",
"affinity": "TEXT",
"defaultValue": "'1'"
},
{
"fieldPath": "customChallengeRatingDescription",
"columnName": "custom_challenge_rating_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "customProficiencyBonus",
"columnName": "custom_proficiency_bonus",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "telepathyRange",
"columnName": "telepathy_range",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "understandsButDescription",
"columnName": "understands_but_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "senses",
"columnName": "senses",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "skills",
"columnName": "skills",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageImmunities",
"columnName": "damage_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageResistances",
"columnName": "damage_resistances",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "damageVulnerabilities",
"columnName": "damage_vulnerabilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "conditionImmunities",
"columnName": "condition_immunities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "languages",
"columnName": "languages",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "abilities",
"columnName": "abilities",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "lairActions",
"columnName": "lair_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActions",
"columnName": "legendary_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "regionalActions",
"columnName": "regional_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "sourceUrl",
"columnName": "source_url",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "bonusActions",
"columnName": "bonus_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "mythicActions",
"columnName": "mythic_actions",
"affinity": "TEXT",
"defaultValue": "'[]'"
},
{
"fieldPath": "legendaryActionsDescription",
"columnName": "legendary_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsDescription",
"columnName": "lair_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "lairActionsEndNote",
"columnName": "lair_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsDescription",
"columnName": "regional_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "regionalActionsEndNote",
"columnName": "regional_actions_end_note",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "mythicActionsDescription",
"columnName": "mythic_actions_description",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "playerName",
"columnName": "player_name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "background",
"columnName": "background",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "personalityTraits",
"columnName": "personality_traits",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "ideals",
"columnName": "ideals",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "bonds",
"columnName": "bonds",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "flaws",
"columnName": "flaws",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "age",
"columnName": "age",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "eyes",
"columnName": "eyes",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "skin",
"columnName": "skin",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "hair",
"columnName": "hair",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "appearance",
"columnName": "appearance",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "backstory",
"columnName": "backstory",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "alliesAndOrganizations",
"columnName": "allies_and_organizations",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "monsters_fts",
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT"
},
{
"fieldPath": "subtype",
"columnName": "subtype",
"affinity": "TEXT"
},
{
"fieldPath": "alignment",
"columnName": "alignment",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"ftsVersion": "FTS4",
"ftsOptions": {
"tokenizer": "simple",
"tokenizerArgs": [],
"contentTable": "monsters",
"languageIdColumnName": "",
"matchInfo": "FTS4",
"notIndexedColumns": [],
"prefixSizes": [],
"preferredOrder": "ASC"
},
"contentSyncTriggers": [
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END"
]
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "collection_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collection_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_monsters_collection_id",
"unique": false,
"columnNames": [
"collection_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)"
},
{
"name": "index_collection_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "collections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collection_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "dashboard_monsters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "monsterId",
"columnName": "monster_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ordinal",
"columnName": "ordinal",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_dashboard_monsters_monster_id",
"unique": false,
"columnNames": [
"monster_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)"
}
],
"foreignKeys": [
{
"table": "monsters",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"monster_id"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '38f6bbd6f1e298b20e17b3a08fbc7f70')"
]
}
}

View File

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

View File

@@ -0,0 +1,37 @@
package com.majinnaibu.monstercards;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import com.majinnaibu.monstercards.data.CollectionDAO;
import com.majinnaibu.monstercards.data.DashboardDAO;
import com.majinnaibu.monstercards.data.MonsterDAO;
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter;
import com.majinnaibu.monstercards.data.converters.ListOfTraitsConverter;
import com.majinnaibu.monstercards.data.converters.SetOfLanguageConverter;
import com.majinnaibu.monstercards.data.converters.SetOfSkillConverter;
import com.majinnaibu.monstercards.data.converters.SetOfStringConverter;
import com.majinnaibu.monstercards.data.converters.UUIDConverter;
import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.MonsterFTS;
@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 8)
@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();
public abstract CollectionDAO collectionDAO();
public abstract DashboardDAO dashboardDAO();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
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);
@Query("DELETE FROM monsters")
Completable deleteAllMonsters();
}

View File

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

View File

@@ -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,43 @@
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) {
if (string == null) {
return AbilityScore.STRENGTH;
}
String lower = string.trim().toLowerCase(java.util.Locale.ROOT);
for (AbilityScore abilityScore : values()) {
if (abilityScore.stringValue.equalsIgnoreCase(lower)
|| abilityScore.shortDisplayName.equalsIgnoreCase(lower)
|| abilityScore.name().equalsIgnoreCase(lower)) {
return abilityScore;
}
}
if (lower.startsWith("str")) return STRENGTH;
if (lower.startsWith("dex")) return DEXTERITY;
if (lower.startsWith("con")) return CONSTITUTION;
if (lower.startsWith("int")) return INTELLIGENCE;
if (lower.startsWith("wis")) return WISDOM;
if (lower.startsWith("cha")) return CHARISMA;
return AbilityScore.STRENGTH;
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,32 @@
package com.majinnaibu.monstercards.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SearchResultItem {
public enum Type {
MONSTER,
COLLECTION
}
@NonNull
public final Type type;
@Nullable
public final Monster monster;
@Nullable
public final Collection collection;
public SearchResultItem(@NonNull Monster monster) {
this.type = Type.MONSTER;
this.monster = monster;
this.collection = null;
}
public SearchResultItem(@NonNull Collection collection) {
this.type = Type.COLLECTION;
this.monster = null;
this.collection = collection;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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