Merge commit 'd1e3c3f5f313057e5a81a4333906ef5d79adea83' as 'Android'

This commit is contained in:
Tom Hicks
2025-06-30 12:23:51 -07:00
192 changed files with 15048 additions and 0 deletions

17
Android/.gitignore vendored Normal file
View File

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

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

@@ -0,0 +1 @@
MonsterCards

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

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

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

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

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

@@ -0,0 +1,22 @@
<?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="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</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>

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

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

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

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<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>

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

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

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

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

4
Android/README.md Normal file
View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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