Compare commits
39 Commits
ccb06960a2
...
e3b6196349
| Author | SHA1 | Date | |
|---|---|---|---|
| e3b6196349 | |||
| baed51259e | |||
| ba77d1b787 | |||
| 85aafd8a02 | |||
| 71a2146c4b | |||
| 509ac11749 | |||
| 5ee7f02b42 | |||
| a7f1ff712a | |||
| 2385f1d088 | |||
| c247c2d998 | |||
| 35f45513aa | |||
| a7b0e68197 | |||
| f74e21e7d9 | |||
| cb26cec7bc | |||
| 8249c68266 | |||
| 338a7e4c0e | |||
| d099d878a0 | |||
| 339e413f82 | |||
| e888bbf8a8 | |||
| a5bf6639e8 | |||
| 9dd5c7e627 | |||
| 9a38c6d98f | |||
| 8f1689574c | |||
| 9c1e27d473 | |||
| bd933ca671 | |||
| 70d549f929 | |||
| c674e2c27a | |||
| a3998d6c1d | |||
| 77176a6d4f | |||
| fa25c1bef3 | |||
| 0e21554773 | |||
| 0cbd03da69 | |||
| c06b53ed6b | |||
| 2d043320e0 | |||
| 1e0c9ece0f | |||
| 65d1b64e0b | |||
| ddb58622d6 | |||
| d63b125175 | |||
| b66b653982 |
7
.gitignore
vendored
@@ -1,3 +1,10 @@
|
|||||||
|
# OS / System
|
||||||
.DS_Store
|
.DS_Store
|
||||||
._*
|
._*
|
||||||
thumbs.db
|
thumbs.db
|
||||||
|
|
||||||
|
# Build & Tooling
|
||||||
|
.artifacts/
|
||||||
|
.worktrees/
|
||||||
|
.build/
|
||||||
|
|
||||||
|
|||||||
18
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# User-specific and local IDE state files
|
||||||
|
/workspace.xml
|
||||||
|
/shelf/
|
||||||
|
/caches/
|
||||||
|
/deviceManager.xml
|
||||||
|
/deploymentTargetSelector.xml
|
||||||
|
/appInsightsSettings.xml
|
||||||
|
/planningMode.xml
|
||||||
|
/studiobot.xml
|
||||||
|
/markdown.xml
|
||||||
|
/assetWizardSettings.xml
|
||||||
|
/usage.statistics.xml
|
||||||
|
/tasks.xml
|
||||||
|
|
||||||
|
# IntelliJ module files generated by Gradle
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
/modules.xml
|
||||||
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
MonsterCards
|
||||||
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
18
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/Android" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/Android" />
|
||||||
|
<option value="$PROJECT_DIR$/Android/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
60
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="NullableNotNullManager">
|
||||||
|
<option name="myDefaultNullable" value="androidx.annotation.Nullable" />
|
||||||
|
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
|
||||||
|
<option name="myOrdered" value="true" />
|
||||||
|
<option name="myNullables">
|
||||||
|
<value>
|
||||||
|
<list size="17">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="android.annotation.Nullable" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
|
||||||
|
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
|
||||||
|
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||||
|
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||||
|
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||||
|
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||||
|
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
|
||||||
|
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||||
|
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
|
||||||
|
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||||
|
<item index="16" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myNotNulls">
|
||||||
|
<value>
|
||||||
|
<list size="16">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="android.annotation.NonNull" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
||||||
|
<item index="7" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
|
||||||
|
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||||
|
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||||
|
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||||
|
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||||
|
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
|
||||||
|
<item index="13" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||||
|
<item index="14" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
|
||||||
|
<item index="15" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
17
.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
68
.idea/runConfigurations/app.xml
generated
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false" focusToolWindowBeforeRun="true">
|
||||||
|
<module name="MonsterCards.app" />
|
||||||
|
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
|
||||||
|
<option name="DEPLOY" value="true" />
|
||||||
|
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
||||||
|
<option name="DEPLOY_AS_INSTANT" value="false" />
|
||||||
|
<option name="ARTIFACT_NAME" value="" />
|
||||||
|
<option name="PM_INSTALL_OPTIONS" value="" />
|
||||||
|
<option name="ALL_USERS" value="false" />
|
||||||
|
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
|
||||||
|
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
|
||||||
|
<option name="CLEAR_APP_STORAGE" value="false" />
|
||||||
|
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
|
||||||
|
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
|
||||||
|
<option name="MODE" value="default_activity" />
|
||||||
|
<option name="RESTORE_ENABLED" value="false" />
|
||||||
|
<option name="RESTORE_FILE" value="" />
|
||||||
|
<option name="RESTORE_FRESH_INSTALL_ONLY" value="false" />
|
||||||
|
<option name="CLEAR_LOGCAT" value="true" />
|
||||||
|
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
|
||||||
|
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||||
|
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||||
|
<Auto>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="true" />
|
||||||
|
</Auto>
|
||||||
|
<Hybrid>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
||||||
|
</Hybrid>
|
||||||
|
<Java>
|
||||||
|
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
||||||
|
</Java>
|
||||||
|
<Native>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
||||||
|
</Native>
|
||||||
|
<Profilers>
|
||||||
|
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="LeakCanary" />
|
||||||
|
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
||||||
|
</Profilers>
|
||||||
|
<option name="DEEP_LINK" value="" />
|
||||||
|
<option name="ACTIVITY" value="" />
|
||||||
|
<option name="ACTIVITY_CLASS" value="" />
|
||||||
|
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
|
||||||
|
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
33
Android/.gitignore
vendored
@@ -1,20 +1,15 @@
|
|||||||
.artifacts
|
# Build outputs
|
||||||
.gradle
|
build/
|
||||||
.idea/appInsightsSettings.xml
|
app/build/
|
||||||
.idea/caches
|
.externalNativeBuild/
|
||||||
.idea/compiler.xml
|
.cxx/
|
||||||
.idea/deploymentTargetSelector.xml
|
captures/
|
||||||
.idea/deviceManager.xml
|
|
||||||
.idea/gradle.xml
|
|
||||||
.idea/markdown.xml
|
|
||||||
.idea/migrations.xml
|
|
||||||
.idea/planningMode.xml
|
|
||||||
.idea/studiobot.xml
|
|
||||||
.idea/workspace.xml
|
|
||||||
.DS_Store
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
|
||||||
*.iml
|
|
||||||
build
|
|
||||||
captures
|
|
||||||
local.properties
|
local.properties
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Android / Gradle caches
|
||||||
|
.gradle/
|
||||||
|
.artifacts/
|
||||||
|
|
||||||
|
# System
|
||||||
|
.DS_Store
|
||||||
|
|||||||
19
Android/.idea/.gitignore
generated
vendored
@@ -1,3 +1,18 @@
|
|||||||
# Default ignored files
|
# User-specific and local IDE state files
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
|
/shelf/
|
||||||
|
/caches/
|
||||||
|
/deviceManager.xml
|
||||||
|
/deploymentTargetSelector.xml
|
||||||
|
/appInsightsSettings.xml
|
||||||
|
/planningMode.xml
|
||||||
|
/studiobot.xml
|
||||||
|
/markdown.xml
|
||||||
|
/assetWizardSettings.xml
|
||||||
|
/usage.statistics.xml
|
||||||
|
/tasks.xml
|
||||||
|
|
||||||
|
# IntelliJ module files generated by Gradle
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
/modules.xml
|
||||||
|
|||||||
6
Android/.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
18
Android/.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
Android/.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -11,21 +11,16 @@ if (propertiesFile.exists()) {
|
|||||||
def appCenterLocalSecret = properties.getProperty('appCenter.localSecret')
|
def appCenterLocalSecret = properties.getProperty('appCenter.localSecret')
|
||||||
def appCenterEnvSecret = System.getenv('APPCENTER_SECRET')
|
def appCenterEnvSecret = System.getenv('APPCENTER_SECRET')
|
||||||
def appCenterSecret = appCenterLocalSecret != null ? appCenterLocalSecret : appCenterEnvSecret != null ? appCenterEnvSecret : ""
|
def appCenterSecret = appCenterLocalSecret != null ? appCenterLocalSecret : appCenterEnvSecret != null ? appCenterEnvSecret : ""
|
||||||
def appCenterSdkVersion = '5.0.4'
|
|
||||||
def nav_version = '2.9.8'
|
|
||||||
def room_version = '2.8.4'
|
|
||||||
def rxjava_version = '3.1.8'
|
|
||||||
def rxandroid_version = '3.0.2'
|
|
||||||
def gson_version = '2.11.0'
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.majinnaibu.monstercards"
|
namespace "com.majinnaibu.monstercards"
|
||||||
compileSdk 35
|
compileSdk 37
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.majinnaibu.monstercards"
|
applicationId "com.majinnaibu.monstercards"
|
||||||
minSdkVersion 23
|
minSdkVersion 24
|
||||||
targetSdk 35
|
//noinspection OldTargetApi
|
||||||
|
targetSdk 36
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\""
|
buildConfigField "String", "APPCENTER_SECRET", "\"${appCenterSecret}\""
|
||||||
@@ -76,35 +71,35 @@ dependencies {
|
|||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
|
|
||||||
// Google
|
// Google
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.8.0'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.14.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.2.2'
|
||||||
implementation "androidx.navigation:navigation-fragment:$nav_version"
|
implementation "androidx.navigation:navigation-fragment:2.10.1"
|
||||||
implementation "androidx.navigation:navigation-ui:$nav_version"
|
implementation "androidx.navigation:navigation-ui:2.10.1"
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.11.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0'
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
||||||
|
|
||||||
// Room DB
|
// Room DB
|
||||||
implementation "io.reactivex.rxjava3:rxjava:$rxjava_version"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.12"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:$rxandroid_version"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:2.8.5"
|
||||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
annotationProcessor "androidx.room:room-compiler:2.8.5"
|
||||||
implementation "androidx.room:room-rxjava3:$room_version"
|
implementation "androidx.room:room-rxjava3:2.8.5"
|
||||||
//testImplementation "androidx.room:room-testing:$room_version"
|
//testImplementation "androidx.room:room-testing:2.8.5"
|
||||||
|
|
||||||
// AppCenter
|
// AppCenter
|
||||||
debugImplementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}"
|
debugImplementation "com.microsoft.appcenter:appcenter-analytics:5.0.6"
|
||||||
debugImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
debugImplementation "com.microsoft.appcenter:appcenter-crashes:5.0.6"
|
||||||
|
|
||||||
// Other 3rd Party
|
// Other 3rd Party
|
||||||
implementation 'com.atlassian.commonmark:commonmark:0.15.2'
|
implementation 'com.atlassian.commonmark:commonmark:0.17.0'
|
||||||
implementation "com.google.code.gson:gson:$gson_version"
|
implementation "com.google.code.gson:gson:2.14.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.majinnaibu.monstercards">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
@@ -11,17 +10,24 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:intentMatchingFlags="enforceIntentFilter"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:name=".SplashActivity"
|
||||||
|
android:theme="@style/Theme.App.Starting"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- Intent filter for viewing/opening content:// URIs with binary/octet-stream MIME type (.monster) -->
|
<!-- Intent filter for viewing/opening content:// URIs with binary/octet-stream MIME type (.monster) -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -48,6 +54,8 @@
|
|||||||
<data android:pathPattern=".*\\.monster\\.txt" />
|
<data android:pathPattern=".*\\.monster\\.txt" />
|
||||||
<data android:pathPattern=".*\\.card" />
|
<data android:pathPattern=".*\\.card" />
|
||||||
<data android:pathPattern=".*\\.card\\.txt" />
|
<data android:pathPattern=".*\\.card\\.txt" />
|
||||||
|
<data android:pathPattern=".*\\.binder" />
|
||||||
|
<data android:pathPattern=".*\\.binder\\.txt" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->
|
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->
|
||||||
|
|||||||
@@ -6,39 +6,61 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.navigation.ui.AppBarConfiguration;
|
import androidx.navigation.ui.AppBarConfiguration;
|
||||||
import androidx.navigation.ui.NavigationUI;
|
import androidx.navigation.ui.NavigationUI;
|
||||||
|
|
||||||
import android.widget.Toast;
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
|
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||||
|
import com.majinnaibu.monstercards.importers.BinderImporter;
|
||||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
||||||
|
import com.majinnaibu.monstercards.importers.GitRepoImporterService;
|
||||||
|
import com.majinnaibu.monstercards.importers.Open5eApiWrapper;
|
||||||
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
||||||
|
import com.majinnaibu.monstercards.models.BinderExport;
|
||||||
|
import com.majinnaibu.monstercards.models.ImportSource;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
import com.majinnaibu.monstercards.utils.ToastHelper;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private Disposable mImportDisposable;
|
||||||
|
private ImportSource mCurrentImportSource;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
if (item.getItemId() == android.R.id.home) {
|
if (item.getItemId() == android.R.id.home) {
|
||||||
@@ -52,10 +74,33 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
setTheme(R.style.AppTheme);
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
AppCenterInitializer.init(getApplication());
|
AppCenterInitializer.init(getApplication());
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
MaterialToolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
if (toolbar != null) {
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
View appBarLayout = findViewById(R.id.app_bar_layout);
|
||||||
BottomNavigationView navView = findViewById(R.id.nav_view);
|
BottomNavigationView navView = findViewById(R.id.nav_view);
|
||||||
|
|
||||||
|
View container = findViewById(R.id.container);
|
||||||
|
if (container != null) {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(container, (v, windowInsets) -> {
|
||||||
|
Insets systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
if (appBarLayout != null) {
|
||||||
|
appBarLayout.setPadding(0, systemBars.top, 0, 0);
|
||||||
|
}
|
||||||
|
if (navView != null) {
|
||||||
|
navView.setPadding(0, 0, 0, systemBars.bottom);
|
||||||
|
}
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
}
|
||||||
// Passing each menu ID as a set of Ids because each
|
// Passing each menu ID as a set of Ids because each
|
||||||
// menu should be considered as top level destinations.
|
// menu should be considered as top level destinations.
|
||||||
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
|
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
|
||||||
@@ -103,29 +148,248 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uri intentUri = getUriFromIntent(intent);
|
||||||
|
String fileName = intentUri != null ? getFileNameFromUri(intentUri) : null;
|
||||||
String json = readMonsterJSONFromIntent(intent);
|
String json = readMonsterJSONFromIntent(intent);
|
||||||
if (!StringHelper.isNullOrEmpty(json)) {
|
if (!StringHelper.isNullOrEmpty(json)) {
|
||||||
importMonsterFromInputAndNavigate(json);
|
importMonsterFromInputAndNavigate(json, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Uri getUriFromIntent(@NonNull Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
Bundle extras = intent.getExtras();
|
||||||
|
if ("android.intent.action.SEND".equals(action)) {
|
||||||
|
if (extras != null) {
|
||||||
|
return extras.getParcelable(Intent.EXTRA_STREAM);
|
||||||
|
}
|
||||||
|
} else if ("android.intent.action.VIEW".equals(action) || "android.intent.action.EDIT".equals(action)) {
|
||||||
|
return intent.getData();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void importMonsterFromInputAndNavigate(@NonNull String input) {
|
public void importMonsterFromInputAndNavigate(@NonNull String input) {
|
||||||
Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show();
|
importMonsterFromInputAndNavigate(input, null);
|
||||||
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
|
}
|
||||||
|
|
||||||
|
public void importMonsterFromInputAndNavigate(@NonNull String input, @Nullable String fileName) {
|
||||||
|
try {
|
||||||
|
BinderImporter binderImporter = new BinderImporter();
|
||||||
|
if (binderImporter.canImport(input)) {
|
||||||
|
ToastHelper.showShort(this, R.string.toast_importing_url);
|
||||||
|
Single.fromCallable(() -> binderImporter.parse(input))
|
||||||
|
.flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
ToastHelper.showLong(this, R.string.snackbar_import_binder_success);
|
||||||
|
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
|
||||||
|
NavController navController = navHostFragment.getNavController();
|
||||||
|
navController.navigate(R.id.navigation_library);
|
||||||
|
}, throwable -> {
|
||||||
|
Logger.logError("Failed to import binder from input: " + fileName, throwable);
|
||||||
|
ToastHelper.showLong(this, R.string.failed_to_import_url);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToastHelper.showShort(this, R.string.toast_importing_url);
|
||||||
|
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input, fileName))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(monster -> {
|
||||||
|
String serializedJson = new Gson().toJson(monster);
|
||||||
|
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
|
||||||
|
NavController navController = navHostFragment.getNavController();
|
||||||
|
NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson);
|
||||||
|
navController.navigate(navAction);
|
||||||
|
}, throwable -> {
|
||||||
|
Logger.logError("Failed to import monster from input: " + fileName, throwable);
|
||||||
|
ToastHelper.showLong(this, R.string.failed_to_import_url);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to process import input: " + fileName, e);
|
||||||
|
ToastHelper.showLong(this, R.string.failed_to_import_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startImportFromSource(ImportSource source) {
|
||||||
|
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.dialog_import_already_running_title)
|
||||||
|
.setMessage(getString(R.string.dialog_import_already_running_message, mCurrentImportSource != null ? mCurrentImportSource.projectName : ""))
|
||||||
|
.setPositiveButton(R.string.action_cancel_current, (dialog, which) -> {
|
||||||
|
mImportDisposable.dispose();
|
||||||
|
executeImportFromSource(source);
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executeImportFromSource(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeImportFromSource(ImportSource source) {
|
||||||
|
mCurrentImportSource = source;
|
||||||
|
View rootView = findViewById(android.R.id.content);
|
||||||
|
Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, getString(R.string.snackbar_importing_source, source.projectName));
|
||||||
|
snackbar.setAction(R.string.action_cancel, v -> {
|
||||||
|
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
|
||||||
|
mImportDisposable.dispose();
|
||||||
|
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_cancelled, source.projectName));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
snackbar.show();
|
||||||
|
|
||||||
|
Callable<Integer> importTask;
|
||||||
|
if (source.importType == ImportSource.ImportType.OPEN5E_API) {
|
||||||
|
importTask = () -> {
|
||||||
|
int totalImported = 0;
|
||||||
|
String nextUrl = null;
|
||||||
|
do {
|
||||||
|
if (mImportDisposable != null && mImportDisposable.isDisposed()) {
|
||||||
|
throw new InterruptedException("Open5e import was cancelled");
|
||||||
|
}
|
||||||
|
Open5eApiWrapper.Open5ePageResult page = Open5eApiWrapper.fetchPage(nextUrl, () -> mImportDisposable != null && mImportDisposable.isDisposed());
|
||||||
|
for (Monster monster : page.monsters) {
|
||||||
|
if (mImportDisposable != null && mImportDisposable.isDisposed()) {
|
||||||
|
throw new InterruptedException("Open5e import was cancelled");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
((MonsterCardsApplication) getApplication()).getMonsterRepository()
|
||||||
|
.saveMonster(monster)
|
||||||
|
.blockingAwait();
|
||||||
|
totalImported++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to save monster to database", e);
|
||||||
|
runOnUiThread(() -> ToastHelper.showShort(MainActivity.this, "An error occurred while saving a monster."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextUrl = page.nextUrl;
|
||||||
|
} while (nextUrl != null && !nextUrl.isEmpty());
|
||||||
|
return totalImported;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
importTask = () -> {
|
||||||
|
try {
|
||||||
|
return GitRepoImporterService.importFromGitRepository(getApplicationContext(), source, () -> mImportDisposable != null && mImportDisposable.isDisposed());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Logger.logError("Import task was interrupted", e);
|
||||||
|
return 0; // Return gracefully on interruption
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mImportDisposable = Single.fromCallable(importTask)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(monster -> {
|
.subscribe(resultCount -> {
|
||||||
String serializedJson = new Gson().toJson(monster);
|
snackbar.dismiss();
|
||||||
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
|
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_complete, resultCount, source.projectName));
|
||||||
NavController navController = navHostFragment.getNavController();
|
|
||||||
NavDirections navAction = MobileNavigationDirections.actionGlobalMonsterImportFragment(serializedJson);
|
|
||||||
navController.navigate(navAction);
|
|
||||||
}, throwable -> {
|
}, throwable -> {
|
||||||
Logger.logError("Failed to import monster from input", throwable);
|
snackbar.dismiss();
|
||||||
Toast.makeText(this, R.string.failed_to_import_url, Toast.LENGTH_LONG).show();
|
if (throwable instanceof InterruptedException) {
|
||||||
|
Logger.logError("Import task was cancelled: " + source.projectName, throwable);
|
||||||
|
// The user cancelled via UI, message handled in the snackbar action.
|
||||||
|
} else {
|
||||||
|
Logger.logError("Failed to import from source: " + source.projectName, throwable);
|
||||||
|
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_failed, source.projectName));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void importMultipleFilesFromUris(@NonNull List<Uri> uris) {
|
||||||
|
if (uris.isEmpty()) return;
|
||||||
|
if (uris.size() == 1) {
|
||||||
|
Uri singleUri = uris.get(0);
|
||||||
|
String fileName = getFileNameFromUri(singleUri);
|
||||||
|
String content = readContentsOfUri(singleUri);
|
||||||
|
if (content != null && !content.trim().isEmpty()) {
|
||||||
|
importMonsterFromInputAndNavigate(content, fileName);
|
||||||
|
} else {
|
||||||
|
ToastHelper.showLong(this, R.string.failed_to_import_url);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToastHelper.showShort(this, R.string.toast_importing_url);
|
||||||
|
Single.fromCallable(() -> {
|
||||||
|
int monstersImported = 0;
|
||||||
|
int bindersImported = 0;
|
||||||
|
|
||||||
|
for (Uri uri : uris) {
|
||||||
|
try {
|
||||||
|
String fileName = getFileNameFromUri(uri);
|
||||||
|
String content = readContentsOfUri(uri);
|
||||||
|
if (content == null || content.trim().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BinderImporter binderImporter = new BinderImporter();
|
||||||
|
if (binderImporter.canImport(content)) {
|
||||||
|
BinderExport binder = binderImporter.parse(content);
|
||||||
|
((MonsterCardsApplication) getApplication()).getMonsterRepository()
|
||||||
|
.importBinder(binder)
|
||||||
|
.blockingAwait();
|
||||||
|
bindersImported++;
|
||||||
|
} else {
|
||||||
|
Monster monster = MonsterImportHelper.fromJSON(content, fileName);
|
||||||
|
((MonsterCardsApplication) getApplication()).getMonsterRepository()
|
||||||
|
.saveMonster(monster)
|
||||||
|
.blockingAwait();
|
||||||
|
monstersImported++;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to import file URI: " + uri, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BatchImportResult(monstersImported, bindersImported);
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> {
|
||||||
|
if (result.bindersImported > 0 || result.monstersImported > 0) {
|
||||||
|
String message = buildImportSummaryMessage(result.monstersImported, result.bindersImported);
|
||||||
|
ToastHelper.showLong(this, message);
|
||||||
|
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
|
||||||
|
NavController navController = navHostFragment.getNavController();
|
||||||
|
navController.navigate(R.id.navigation_library);
|
||||||
|
} else {
|
||||||
|
ToastHelper.showLong(this, R.string.failed_to_import_url);
|
||||||
|
}
|
||||||
|
}, throwable -> {
|
||||||
|
Logger.logError("Failed to execute batch file import", throwable);
|
||||||
|
ToastHelper.showLong(this, R.string.failed_to_import_url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildImportSummaryMessage(int monstersCount, int bindersCount) {
|
||||||
|
StringBuilder sb = new StringBuilder("Successfully imported ");
|
||||||
|
if (bindersCount > 0 && monstersCount > 0) {
|
||||||
|
sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections")
|
||||||
|
.append(" and ")
|
||||||
|
.append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters");
|
||||||
|
} else if (bindersCount > 0) {
|
||||||
|
sb.append(bindersCount).append(bindersCount == 1 ? " collection" : " collections");
|
||||||
|
} else {
|
||||||
|
sb.append(monstersCount).append(monstersCount == 1 ? " monster" : " monsters");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BatchImportResult {
|
||||||
|
final int monstersImported;
|
||||||
|
final int bindersImported;
|
||||||
|
|
||||||
|
BatchImportResult(int monstersImported, int bindersImported) {
|
||||||
|
this.monstersImported = monstersImported;
|
||||||
|
this.bindersImported = bindersImported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String readMonsterJSONFromIntent(@NonNull Intent intent) {
|
private String readMonsterJSONFromIntent(@NonNull Intent intent) {
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
@@ -143,7 +407,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
if (uri == null || !isMonsterFile(uri)) {
|
if (uri == null || !isMonsterFile(uri)) {
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
Logger.logError("Ignored file because extension is not supported (.monster, .card): " + uri);
|
Logger.logError("Ignored file because extension is not supported (.monster, .card, .binder): " + uri);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -162,7 +426,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
||||||
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
|
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
|
||||||
|| lowerName.endsWith(".card") || lowerName.endsWith(".card.txt");
|
|| lowerName.endsWith(".card") || lowerName.endsWith(".card.txt")
|
||||||
|
|| lowerName.endsWith(".binder") || lowerName.endsWith(".binder.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -199,8 +464,17 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logger.logError("error reading file", e);
|
Logger.logError("error reading file", e);
|
||||||
|
ToastHelper.showLong(this, "An error occurred while reading the file.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
|
||||||
|
mImportDisposable.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
|
|||||||
|
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
|
||||||
public class MonsterCardsApplication extends Application {
|
public class MonsterCardsApplication extends Application {
|
||||||
|
|
||||||
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
|
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
|
||||||
@@ -110,6 +116,19 @@ public class MonsterCardsApplication extends Application {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
|
RxJavaPlugins.setErrorHandler(e -> {
|
||||||
|
if (e instanceof UndeliverableException) {
|
||||||
|
e = e.getCause();
|
||||||
|
}
|
||||||
|
if (e instanceof InterruptedException || e instanceof InterruptedIOException) {
|
||||||
|
// Expected when a blocking task is cancelled; swallow silently.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logError("Undeliverable exception received by RxJava global handler", e);
|
||||||
|
});
|
||||||
|
|
||||||
// Required initialization logic here!
|
// Required initialization logic here!
|
||||||
|
|
||||||
// .fallbackToDestructiveMigration()
|
// .fallbackToDestructiveMigration()
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.majinnaibu.monstercards;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
public class SplashActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
setTheme(R.style.AppTheme);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_splash);
|
||||||
|
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
Intent intent = new Intent(SplashActivity.this, MainActivity.class);
|
||||||
|
if (getIntent() != null) {
|
||||||
|
if (getIntent().getExtras() != null) {
|
||||||
|
intent.putExtras(getIntent().getExtras());
|
||||||
|
}
|
||||||
|
if (getIntent().getAction() != null) {
|
||||||
|
intent.setAction(getIntent().getAction());
|
||||||
|
}
|
||||||
|
if (getIntent().getData() != null) {
|
||||||
|
intent.setData(getIntent().getData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,9 +47,18 @@ public interface CollectionDAO {
|
|||||||
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId")
|
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId AND monster_id = :monsterId")
|
||||||
Completable removeMonsterFromCollection(String collectionId, String monsterId);
|
Completable removeMonsterFromCollection(String collectionId, String monsterId);
|
||||||
|
|
||||||
|
@Query("DELETE FROM collection_monsters WHERE collection_id = :collectionId")
|
||||||
|
Completable removeAllMonstersFromCollection(String collectionId);
|
||||||
|
|
||||||
@Query("DELETE FROM collection_monsters WHERE id = :id")
|
@Query("DELETE FROM collection_monsters WHERE id = :id")
|
||||||
Completable removeCollectionMonsterById(long id);
|
Completable removeCollectionMonsterById(long id);
|
||||||
|
|
||||||
|
@Query("DELETE FROM collection_monsters")
|
||||||
|
Completable deleteAllCollectionMonsters();
|
||||||
|
|
||||||
|
@Query("DELETE FROM collections")
|
||||||
|
Completable deleteAllCollections();
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters);
|
Completable updateCollectionMonsters(List<CollectionMonster> collectionMonsters);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package com.majinnaibu.monstercards.data;
|
|||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Transaction;
|
||||||
import androidx.room.Update;
|
import androidx.room.Update;
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.models.DashboardMonster;
|
import com.majinnaibu.monstercards.models.DashboardMonster;
|
||||||
|
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -19,6 +21,10 @@ public interface DashboardDAO {
|
|||||||
@Query("SELECT monsters.* FROM monsters INNER JOIN dashboard_monsters ON monsters.id = dashboard_monsters.monster_id ORDER BY dashboard_monsters.ordinal ASC, dashboard_monsters.id ASC")
|
@Query("SELECT monsters.* FROM monsters INNER JOIN dashboard_monsters ON monsters.id = dashboard_monsters.monster_id ORDER BY dashboard_monsters.ordinal ASC, dashboard_monsters.id ASC")
|
||||||
Flowable<List<Monster>> getDashboardMonsters();
|
Flowable<List<Monster>> getDashboardMonsters();
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
|
||||||
|
Flowable<List<DashboardMonsterWithMonster>> getDashboardMonstersWithMonster();
|
||||||
|
|
||||||
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
|
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
|
||||||
Flowable<List<DashboardMonster>> getDashboardMonsterEntries();
|
Flowable<List<DashboardMonster>> getDashboardMonsterEntries();
|
||||||
|
|
||||||
@@ -28,9 +34,12 @@ public interface DashboardDAO {
|
|||||||
@Query("DELETE FROM dashboard_monsters WHERE id = :id")
|
@Query("DELETE FROM dashboard_monsters WHERE id = :id")
|
||||||
Completable removeDashboardMonsterById(long id);
|
Completable removeDashboardMonsterById(long id);
|
||||||
|
|
||||||
@Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId")
|
@Query("DELETE FROM dashboard_monsters WHERE id = (SELECT id FROM dashboard_monsters WHERE monster_id = :monsterId LIMIT 1)")
|
||||||
Completable removeMonsterFromDashboard(String monsterId);
|
Completable removeMonsterFromDashboard(String monsterId);
|
||||||
|
|
||||||
|
@Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId")
|
||||||
|
Completable removeAllMonstersFromDashboard(String monsterId);
|
||||||
|
|
||||||
@Query("DELETE FROM dashboard_monsters")
|
@Query("DELETE FROM dashboard_monsters")
|
||||||
Completable clearDashboard();
|
Completable clearDashboard();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.majinnaibu.monstercards.data;
|
||||||
|
|
||||||
|
import com.majinnaibu.monstercards.models.ImportSource;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ImportConfig {
|
||||||
|
public static final List<ImportSource> SOURCES = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
SOURCES.add(new ImportSource(
|
||||||
|
"open5e",
|
||||||
|
"Open5e.com System Reference Document",
|
||||||
|
"Open5e",
|
||||||
|
"https://open5e.com",
|
||||||
|
ImportSource.ImportType.OPEN5E_API,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
));
|
||||||
|
|
||||||
|
SOURCES.add(new ImportSource(
|
||||||
|
"pf2e_foundry",
|
||||||
|
"Pathfinder 2e Foundry VTT",
|
||||||
|
"foundryvtt",
|
||||||
|
"https://github.com/foundryvtt/pf2e",
|
||||||
|
ImportSource.ImportType.GIT_ARCHIVE,
|
||||||
|
"https://github.com/foundryvtt/pf2e/archive/refs/heads/v14-dev.zip",
|
||||||
|
"com.majinnaibu.monstercards.importers.Pf2eImporter",
|
||||||
|
".json",
|
||||||
|
"pf2e-14-dev/packs/pf2e"
|
||||||
|
));
|
||||||
|
|
||||||
|
SOURCES.add(new ImportSource(
|
||||||
|
"sf2e_foundry",
|
||||||
|
"Starfinder 2e Foundry VTT",
|
||||||
|
"foundryvtt",
|
||||||
|
"https://github.com/foundryvtt/pf2e",
|
||||||
|
ImportSource.ImportType.GIT_ARCHIVE,
|
||||||
|
"https://github.com/foundryvtt/pf2e/archive/refs/heads/v14-dev.zip",
|
||||||
|
"com.majinnaibu.monstercards.importers.Pf2eImporter",
|
||||||
|
".json",
|
||||||
|
"pf2e-14-dev/packs/sf2e"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,4 +36,7 @@ public interface MonsterDAO {
|
|||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
Completable delete(Monster monster);
|
Completable delete(Monster monster);
|
||||||
|
|
||||||
|
@Query("DELETE FROM monsters")
|
||||||
|
Completable deleteAllMonsters();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,27 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import com.majinnaibu.monstercards.AppDatabase;
|
import com.majinnaibu.monstercards.AppDatabase;
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||||
|
import com.majinnaibu.monstercards.models.BinderExport;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
import com.majinnaibu.monstercards.models.Collection;
|
||||||
import com.majinnaibu.monstercards.models.CollectionMonster;
|
import com.majinnaibu.monstercards.models.CollectionMonster;
|
||||||
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
||||||
import com.majinnaibu.monstercards.models.DashboardMonster;
|
import com.majinnaibu.monstercards.models.DashboardMonster;
|
||||||
|
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.models.SearchResultItem;
|
import com.majinnaibu.monstercards.models.SearchResultItem;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Completable;
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
@@ -205,6 +212,13 @@ public class MonsterRepository {
|
|||||||
.observeOn(AndroidSchedulers.mainThread());
|
.observeOn(AndroidSchedulers.mainThread());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Flowable<List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster>> getDashboardMonstersWithMonster() {
|
||||||
|
return m_db.dashboardDAO()
|
||||||
|
.getDashboardMonstersWithMonster()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
public Flowable<List<DashboardMonster>> getDashboardMonsterEntries() {
|
public Flowable<List<DashboardMonster>> getDashboardMonsterEntries() {
|
||||||
return m_db.dashboardDAO()
|
return m_db.dashboardDAO()
|
||||||
.getDashboardMonsterEntries()
|
.getDashboardMonsterEntries()
|
||||||
@@ -254,12 +268,257 @@ public class MonsterRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Completable clearAllData() {
|
||||||
|
return Completable.fromAction(() -> {
|
||||||
|
m_db.dashboardDAO().clearDashboard().blockingAwait();
|
||||||
|
m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait();
|
||||||
|
m_db.collectionDAO().deleteAllCollections().blockingAwait();
|
||||||
|
m_db.monsterDAO().deleteAllMonsters().blockingAwait();
|
||||||
|
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Completable removeAllCollections() {
|
||||||
|
return Completable.fromAction(() -> {
|
||||||
|
m_db.collectionDAO().deleteAllCollectionMonsters().blockingAwait();
|
||||||
|
m_db.collectionDAO().deleteAllCollections().blockingAwait();
|
||||||
|
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Completable removeAllMonstersFromCollection(@NonNull UUID collectionId) {
|
||||||
|
Completable result = m_db.collectionDAO().removeAllMonstersFromCollection(collectionId.toString());
|
||||||
|
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<String> exportCollections() {
|
||||||
|
return m_db.collectionDAO().getAll().first(new ArrayList<>())
|
||||||
|
.map(collections -> {
|
||||||
|
List<BinderExport.CollectionExport> colExports = new ArrayList<>();
|
||||||
|
for (Collection col : collections) {
|
||||||
|
List<Monster> colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString())
|
||||||
|
.first(new ArrayList<>()).blockingGet();
|
||||||
|
colExports.add(new BinderExport.CollectionExport(
|
||||||
|
col.id.toString(),
|
||||||
|
col.name,
|
||||||
|
col.description,
|
||||||
|
colMonsters
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, null);
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<String> exportEverything() {
|
||||||
|
return Single.zip(
|
||||||
|
m_db.collectionDAO().getAll().first(new ArrayList<>()),
|
||||||
|
m_db.monsterDAO().getAll().first(new ArrayList<>()),
|
||||||
|
m_db.dashboardDAO().getDashboardMonsters().first(new ArrayList<>()),
|
||||||
|
(collections, allMonsters, dashboardMonsters) -> {
|
||||||
|
List<BinderExport.CollectionExport> colExports = new ArrayList<>();
|
||||||
|
Set<UUID> monstersInCollections = new HashSet<>();
|
||||||
|
|
||||||
|
for (Collection col : collections) {
|
||||||
|
List<Monster> colMonsters = m_db.collectionDAO().getMonstersForCollection(col.id.toString())
|
||||||
|
.first(new ArrayList<>()).blockingGet();
|
||||||
|
for (Monster m : colMonsters) {
|
||||||
|
monstersInCollections.add(m.id);
|
||||||
|
}
|
||||||
|
colExports.add(new BinderExport.CollectionExport(
|
||||||
|
col.id.toString(),
|
||||||
|
col.name,
|
||||||
|
col.description,
|
||||||
|
colMonsters
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Monster> uncategorized = new ArrayList<>();
|
||||||
|
for (Monster m : allMonsters) {
|
||||||
|
if (!monstersInCollections.contains(m.id)) {
|
||||||
|
uncategorized.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uncategorized.isEmpty() || colExports.isEmpty()) {
|
||||||
|
colExports.add(new BinderExport.CollectionExport(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
colExports.isEmpty() ? "All Monsters" : "Uncategorized",
|
||||||
|
"",
|
||||||
|
colExports.isEmpty() ? allMonsters : uncategorized
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new com.majinnaibu.monstercards.exporters.BinderExporter().exportFullBackup(colExports, dashboardMonsters);
|
||||||
|
}
|
||||||
|
).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
public Completable updateDashboardMonsters(List<DashboardMonster> items) {
|
public Completable updateDashboardMonsters(List<DashboardMonster> items) {
|
||||||
Completable result = m_db.dashboardDAO().updateDashboardMonsters(items);
|
Completable result = m_db.dashboardDAO().updateDashboardMonsters(items);
|
||||||
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Completable importBinder(@NonNull BinderExport binder) {
|
||||||
|
return Completable.fromAction(() -> {
|
||||||
|
// 1. Process Collections & Cards in Binder
|
||||||
|
if (binder.collections != null && !binder.collections.isEmpty()) {
|
||||||
|
List<Collection> existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
|
||||||
|
|
||||||
|
for (BinderExport.CollectionExport colExport : binder.collections) {
|
||||||
|
if (colExport == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String colName = colExport.name != null ? colExport.name.trim() : "";
|
||||||
|
String colDesc = colExport.description != null ? colExport.description.trim() : "";
|
||||||
|
|
||||||
|
UUID colExportId = null;
|
||||||
|
if (colExport.id != null && !colExport.id.trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
colExportId = UUID.fromString(colExport.id.trim());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection targetCollection = null;
|
||||||
|
if (colExportId != null) {
|
||||||
|
for (Collection existing : existingCols) {
|
||||||
|
if (existing.id.equals(colExportId)) {
|
||||||
|
targetCollection = existing;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetCollection == null && !colName.isEmpty()) {
|
||||||
|
for (Collection existing : existingCols) {
|
||||||
|
if (existing.name != null && existing.name.equalsIgnoreCase(colName)) {
|
||||||
|
targetCollection = existing;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID targetCollectionId;
|
||||||
|
if (targetCollection != null) {
|
||||||
|
targetCollectionId = targetCollection.id;
|
||||||
|
if (!colDesc.isEmpty() && (targetCollection.description == null || targetCollection.description.isEmpty())) {
|
||||||
|
targetCollection.description = colDesc;
|
||||||
|
m_db.collectionDAO().save(targetCollection).blockingAwait();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Collection newCol = new Collection();
|
||||||
|
newCol.id = colExportId != null ? colExportId : UUID.randomUUID();
|
||||||
|
newCol.name = !colName.isEmpty() ? colName : "Imported Collection";
|
||||||
|
newCol.description = colDesc;
|
||||||
|
m_db.collectionDAO().save(newCol).blockingAwait();
|
||||||
|
targetCollectionId = newCol.id;
|
||||||
|
existingCols.add(newCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colExport.cards != null && !colExport.cards.isEmpty()) {
|
||||||
|
List<Monster> monstersToSave = new ArrayList<>();
|
||||||
|
Map<UUID, Integer> importedCardCounts = new HashMap<>();
|
||||||
|
|
||||||
|
for (Monster card : colExport.cards) {
|
||||||
|
if (card != null) {
|
||||||
|
if (card.id == null) {
|
||||||
|
card.id = UUID.randomUUID();
|
||||||
|
}
|
||||||
|
monstersToSave.add(card);
|
||||||
|
importedCardCounts.put(card.id, getCount(importedCardCounts, card.id) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monstersToSave.isEmpty()) {
|
||||||
|
m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait();
|
||||||
|
|
||||||
|
List<Monster> existingColMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString())
|
||||||
|
.first(new ArrayList<>()).blockingGet();
|
||||||
|
Map<UUID, Integer> existingCardCounts = new HashMap<>();
|
||||||
|
for (Monster m : existingColMonsters) {
|
||||||
|
existingCardCounts.put(m.id, getCount(existingCardCounts, m.id) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ordinal = existingColMonsters.size();
|
||||||
|
for (Map.Entry<UUID, Integer> entry : importedCardCounts.entrySet()) {
|
||||||
|
UUID monsterId = entry.getKey();
|
||||||
|
int importedCount = entry.getValue();
|
||||||
|
int existingCount = getCount(existingCardCounts, monsterId);
|
||||||
|
|
||||||
|
if (importedCount > existingCount) {
|
||||||
|
int extraCopiesNeeded = importedCount - existingCount;
|
||||||
|
for (int k = 0; k < extraCopiesNeeded; k++) {
|
||||||
|
m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monsterId, ordinal++)).blockingAwait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Process Dashboard entries in Binder if present
|
||||||
|
if (binder.dashboard != null && !binder.dashboard.isEmpty()) {
|
||||||
|
List<Monster> dashboardMonstersToSave = new ArrayList<>();
|
||||||
|
Map<UUID, Integer> importedDashboardCounts = new HashMap<>();
|
||||||
|
|
||||||
|
for (Monster card : binder.dashboard) {
|
||||||
|
if (card != null) {
|
||||||
|
if (card.id == null) {
|
||||||
|
card.id = UUID.randomUUID();
|
||||||
|
}
|
||||||
|
dashboardMonstersToSave.add(card);
|
||||||
|
importedDashboardCounts.put(card.id, getCount(importedDashboardCounts, card.id) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dashboardMonstersToSave.isEmpty()) {
|
||||||
|
m_db.monsterDAO().save(dashboardMonstersToSave.toArray(new Monster[0])).blockingAwait();
|
||||||
|
|
||||||
|
List<DashboardMonsterWithMonster> existingDashboard = m_db.dashboardDAO().getDashboardMonstersWithMonster()
|
||||||
|
.first(new ArrayList<>()).blockingGet();
|
||||||
|
Map<UUID, Integer> existingDashboardCounts = new HashMap<>();
|
||||||
|
int currentMaxOrdinal = 0;
|
||||||
|
for (DashboardMonsterWithMonster entry : existingDashboard) {
|
||||||
|
if (entry.monster != null) {
|
||||||
|
existingDashboardCounts.put(entry.monster.id, getCount(existingDashboardCounts, entry.monster.id) + 1);
|
||||||
|
}
|
||||||
|
if (entry.dashboardEntry != null && entry.dashboardEntry.ordinal > currentMaxOrdinal) {
|
||||||
|
currentMaxOrdinal = entry.dashboardEntry.ordinal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ordinal = currentMaxOrdinal + 1;
|
||||||
|
List<DashboardMonster> extraEntriesToAdd = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Map.Entry<UUID, Integer> entry : importedDashboardCounts.entrySet()) {
|
||||||
|
UUID monsterId = entry.getKey();
|
||||||
|
int importedCount = entry.getValue();
|
||||||
|
int existingCount = getCount(existingDashboardCounts, monsterId);
|
||||||
|
|
||||||
|
if (importedCount > existingCount) {
|
||||||
|
int extraNeeded = importedCount - existingCount;
|
||||||
|
for (int k = 0; k < extraNeeded; k++) {
|
||||||
|
extraEntriesToAdd.add(new DashboardMonster(monsterId, ordinal++));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extraEntriesToAdd.isEmpty()) {
|
||||||
|
m_db.dashboardDAO().addMonsterToDashboard(extraEntriesToAdd.toArray(new DashboardMonster[0])).blockingAwait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getCount(Map<UUID, Integer> map, UUID key) {
|
||||||
|
if (map == null || key == null) return 0;
|
||||||
|
Integer val = map.get(key);
|
||||||
|
return val != null ? val : 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static class Helpers {
|
private static class Helpers {
|
||||||
static boolean monsterMatchesSearch(Monster monster, String searchText) {
|
static boolean monsterMatchesSearch(Monster monster, String searchText) {
|
||||||
if (StringHelper.isNullOrEmpty(searchText)) {
|
if (StringHelper.isNullOrEmpty(searchText)) {
|
||||||
|
|||||||
@@ -21,11 +21,23 @@ public enum AbilityScore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static AbilityScore valueOfString(String string) {
|
public static AbilityScore valueOfString(String string) {
|
||||||
|
if (string == null) {
|
||||||
|
return AbilityScore.STRENGTH;
|
||||||
|
}
|
||||||
|
String lower = string.trim().toLowerCase(java.util.Locale.ROOT);
|
||||||
for (AbilityScore abilityScore : values()) {
|
for (AbilityScore abilityScore : values()) {
|
||||||
if (abilityScore.stringValue.equals(string)) {
|
if (abilityScore.stringValue.equalsIgnoreCase(lower)
|
||||||
|
|| abilityScore.shortDisplayName.equalsIgnoreCase(lower)
|
||||||
|
|| abilityScore.name().equalsIgnoreCase(lower)) {
|
||||||
return abilityScore;
|
return abilityScore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (lower.startsWith("str")) return STRENGTH;
|
||||||
|
if (lower.startsWith("dex")) return DEXTERITY;
|
||||||
|
if (lower.startsWith("con")) return CONSTITUTION;
|
||||||
|
if (lower.startsWith("int")) return INTELLIGENCE;
|
||||||
|
if (lower.startsWith("wis")) return WISDOM;
|
||||||
|
if (lower.startsWith("cha")) return CHARISMA;
|
||||||
return AbilityScore.STRENGTH;
|
return AbilityScore.STRENGTH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.majinnaibu.monstercards.exporters;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.majinnaibu.monstercards.models.BinderExport;
|
||||||
|
import com.majinnaibu.monstercards.models.Collection;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class BinderExporter {
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String exportBinder(String collectionName, List<Monster> monsters) {
|
||||||
|
return exportBinder(new Collection(collectionName != null ? collectionName : "", ""), monsters, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String exportBinder(@Nullable Collection collection, List<Monster> monsters) {
|
||||||
|
return exportBinder(collection, monsters, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String exportBinder(@Nullable Collection collection, @Nullable List<Monster> monsters, @Nullable List<Monster> dashboardMonsters) {
|
||||||
|
BinderExport export = new BinderExport();
|
||||||
|
export.schema = "https://majinnaibu.com/schemas/binder.schema.json";
|
||||||
|
export.schemaVersion = 1;
|
||||||
|
|
||||||
|
if (monsters != null) {
|
||||||
|
for (Monster monster : monsters) {
|
||||||
|
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
|
||||||
|
monster.schemaVersion = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) {
|
||||||
|
for (Monster monster : dashboardMonsters) {
|
||||||
|
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
|
||||||
|
monster.schemaVersion = 1;
|
||||||
|
}
|
||||||
|
export.dashboard = dashboardMonsters;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection != null) {
|
||||||
|
String colId = collection.id.toString();
|
||||||
|
String colName = collection.name;
|
||||||
|
String colDesc = collection.description;
|
||||||
|
|
||||||
|
BinderExport.CollectionExport col = new BinderExport.CollectionExport(
|
||||||
|
colId,
|
||||||
|
colName,
|
||||||
|
colDesc,
|
||||||
|
monsters
|
||||||
|
);
|
||||||
|
export.collections = Collections.singletonList(col);
|
||||||
|
}
|
||||||
|
return GSON.toJson(export);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String exportFullBackup(@Nullable List<BinderExport.CollectionExport> collections, @Nullable List<Monster> dashboardMonsters) {
|
||||||
|
BinderExport export = new BinderExport();
|
||||||
|
export.schema = "https://majinnaibu.com/schemas/binder.schema.json";
|
||||||
|
export.schemaVersion = 1;
|
||||||
|
|
||||||
|
if (collections != null) {
|
||||||
|
for (BinderExport.CollectionExport col : collections) {
|
||||||
|
if (col.cards != null) {
|
||||||
|
for (Monster m : col.cards) {
|
||||||
|
m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
|
||||||
|
m.schemaVersion = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export.collections = collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dashboardMonsters != null && !dashboardMonsters.isEmpty()) {
|
||||||
|
for (Monster m : dashboardMonsters) {
|
||||||
|
m.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
|
||||||
|
m.schemaVersion = 1;
|
||||||
|
}
|
||||||
|
export.dashboard = dashboardMonsters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GSON.toJson(export);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.majinnaibu.monstercards.exporters;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
|
||||||
|
public class MonsterCardExporter {
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String exportCard(@NonNull Monster monster) {
|
||||||
|
monster.schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
|
||||||
|
monster.schemaVersion = 1;
|
||||||
|
return GSON.toJson(monster);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,85 @@
|
|||||||
package com.majinnaibu.monstercards.helpers;
|
package com.majinnaibu.monstercards.helpers;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
||||||
import com.majinnaibu.monstercards.importers.EntityImporter;
|
import com.majinnaibu.monstercards.importers.EntityImporter;
|
||||||
import com.majinnaibu.monstercards.importers.MonsterJsonImporter;
|
import com.majinnaibu.monstercards.importers.MonsterJsonImporter;
|
||||||
import com.majinnaibu.monstercards.importers.Open5eImporter;
|
import com.majinnaibu.monstercards.importers.Open5eImporter;
|
||||||
import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter;
|
import com.majinnaibu.monstercards.importers.TetraCubeMonsterImporter;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class MonsterImportHelper {
|
public class MonsterImportHelper {
|
||||||
private static final List<EntityImporter<Monster>> IMPORTERS = new ArrayList<>();
|
private static final List<EntityImporter<Monster>> IMPORTERS = new ArrayList<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
IMPORTERS.add(new TetraCubeMonsterImporter());
|
||||||
IMPORTERS.add(new MonsterJsonImporter());
|
IMPORTERS.add(new MonsterJsonImporter());
|
||||||
IMPORTERS.add(new Open5eImporter());
|
IMPORTERS.add(new Open5eImporter());
|
||||||
IMPORTERS.add(new TetraCubeMonsterImporter());
|
|
||||||
IMPORTERS.add(new DnDBeyondImporter());
|
IMPORTERS.add(new DnDBeyondImporter());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Monster fromJSON(String json) {
|
public static Monster fromJSON(String json) {
|
||||||
if (json != null) {
|
return fromJSON(json, null);
|
||||||
for (EntityImporter<Monster> importer : IMPORTERS) {
|
}
|
||||||
if (importer.canImport(json)) {
|
|
||||||
try {
|
@NonNull
|
||||||
return importer.parse(json);
|
public static Monster fromJSON(String json, @Nullable String fileName) {
|
||||||
} catch (Exception e) {
|
if (json == null || json.trim().isEmpty()) {
|
||||||
throw new IllegalArgumentException("Failed to parse monster JSON", e);
|
throw new IllegalArgumentException("JSON payload is null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check for explicit $schema reference matching monster-card or tetracube schema
|
||||||
|
try {
|
||||||
|
JsonElement el = JsonParser.parseString(json);
|
||||||
|
if (el.isJsonObject()) {
|
||||||
|
JsonObject obj = el.getAsJsonObject();
|
||||||
|
if (obj.has("$schema")) {
|
||||||
|
String schemaVal = obj.get("$schema").getAsString();
|
||||||
|
if (schemaVal.contains("monster-card.schema.json")) {
|
||||||
|
return new MonsterJsonImporter().parse(json);
|
||||||
|
} else if (schemaVal.contains("tetracube-monster.schema.json")) {
|
||||||
|
return new TetraCubeMonsterImporter().parse(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Error checking $schema reference tag", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If filename ends in .monster, try TetraCube importer first
|
||||||
|
String lowerFileName = fileName != null ? fileName.toLowerCase(Locale.ROOT) : "";
|
||||||
|
boolean isMonsterFile = lowerFileName.endsWith(".monster") || lowerFileName.endsWith(".monster.txt");
|
||||||
|
if (isMonsterFile) {
|
||||||
|
TetraCubeMonsterImporter tetraImporter = new TetraCubeMonsterImporter();
|
||||||
|
if (tetraImporter.canImport(json)) {
|
||||||
|
try {
|
||||||
|
return tetraImporter.parse(json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("TetraCube importer failed for .monster file: " + fileName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to current importer detection chain
|
||||||
|
for (EntityImporter<Monster> importer : IMPORTERS) {
|
||||||
|
if (importer.canImport(json)) {
|
||||||
|
try {
|
||||||
|
return importer.parse(json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Importer " + importer.getClass().getSimpleName() + " failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("No importer found capable of parsing the given JSON payload.");
|
throw new IllegalArgumentException("No importer found capable of parsing the given JSON payload.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.majinnaibu.monstercards.importers;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.majinnaibu.monstercards.models.BinderExport;
|
||||||
|
|
||||||
|
public class BinderImporter implements EntityImporter<BinderExport> {
|
||||||
|
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canImport(@NonNull String input) {
|
||||||
|
try {
|
||||||
|
JsonElement el = JsonParser.parseString(input);
|
||||||
|
if (el.isJsonObject()) {
|
||||||
|
JsonObject obj = el.getAsJsonObject();
|
||||||
|
if (obj.has("$schema")) {
|
||||||
|
String schemaVal = obj.get("$schema").getAsString();
|
||||||
|
if (schemaVal.contains("binder.schema.json")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj.has("collections") && obj.get("collections").isJsonArray();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public BinderExport parse(@NonNull String input) throws Exception {
|
||||||
|
BinderExport binder = GSON.fromJson(input, BinderExport.class);
|
||||||
|
if (binder == null || binder.collections == null) {
|
||||||
|
throw new IllegalArgumentException("Failed to deserialize Binder JSON");
|
||||||
|
}
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package com.majinnaibu.monstercards.importers;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Environment;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
||||||
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
|
import com.majinnaibu.monstercards.models.ImportSource;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
public class GitRepoImporterService {
|
||||||
|
|
||||||
|
public static int importFromGitRepository(Context context, ImportSource source, BooleanSupplier isCancelled) throws Exception {
|
||||||
|
int totalImported = 0;
|
||||||
|
|
||||||
|
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
if (!downloadsDir.exists()) {
|
||||||
|
downloadsDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
File tempZip = null;
|
||||||
|
File[] existingZips = downloadsDir.listFiles((dir, name) -> name.startsWith(source.id + "_download") && name.endsWith(".zip"));
|
||||||
|
if (existingZips != null && existingZips.length > 0) {
|
||||||
|
tempZip = existingZips[0];
|
||||||
|
} else {
|
||||||
|
tempZip = new File(downloadsDir, source.id + "_download_" + System.currentTimeMillis() + ".zip");
|
||||||
|
downloadZip(source.downloadUrl, tempZip, isCancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
File extractDir = new File(context.getCacheDir(), "repo_extracted_" + System.currentTimeMillis());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isCancelled.getAsBoolean()) return 0;
|
||||||
|
|
||||||
|
if (!extractDir.exists()) {
|
||||||
|
extractDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
int extractedFiles = unzipAndFilter(tempZip, extractDir, source.fileExtension, source.subfolder, isCancelled);
|
||||||
|
Logger.logWTF("Extracted " + extractedFiles + " files to " + extractDir.getAbsolutePath());
|
||||||
|
|
||||||
|
if (isCancelled.getAsBoolean()) return 0;
|
||||||
|
|
||||||
|
// Instantiate Importer dynamically
|
||||||
|
Class<?> clazz = Class.forName(source.importerClassName);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
EntityImporter<Monster> importer = (EntityImporter<Monster>) clazz.newInstance();
|
||||||
|
|
||||||
|
MonsterRepository repository = ((MonsterCardsApplication) context.getApplicationContext()).getMonsterRepository();
|
||||||
|
|
||||||
|
// Process files
|
||||||
|
totalImported = processDirectory(extractDir, importer, repository, isCancelled);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// We no longer delete tempZip since the user wants to keep the downloaded zip
|
||||||
|
// deleteFileOrDir(tempZip);
|
||||||
|
// DO NOT DELETE EXTRACT DIR FOR NOW
|
||||||
|
// deleteFileOrDir(extractDir);
|
||||||
|
Logger.logWTF("Skipping deletion of extract directory: " + extractDir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalImported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void downloadZip(String urlStr, File dest, BooleanSupplier isCancelled) throws IOException, InterruptedException {
|
||||||
|
URL url = new URL(urlStr);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(15000);
|
||||||
|
conn.setReadTimeout(30000);
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
// GitHub might redirect archive links, so handle redirect
|
||||||
|
if (code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_SEE_OTHER) {
|
||||||
|
String newUrl = conn.getHeaderField("Location");
|
||||||
|
conn = (HttpURLConnection) new URL(newUrl).openConnection();
|
||||||
|
conn.setConnectTimeout(15000);
|
||||||
|
conn.setReadTimeout(30000);
|
||||||
|
code = conn.getResponseCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code != 200) {
|
||||||
|
throw new IllegalStateException("Git repo download failed with HTTP " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream in = new BufferedInputStream(conn.getInputStream());
|
||||||
|
FileOutputStream out = new FileOutputStream(dest)) {
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int count;
|
||||||
|
while ((count = in.read(buffer)) != -1) {
|
||||||
|
if (isCancelled.getAsBoolean()) {
|
||||||
|
throw new InterruptedException("Git download was cancelled.");
|
||||||
|
}
|
||||||
|
out.write(buffer, 0, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int unzipAndFilter(File zipFile, File extractDir, String filterExtension, String subfolder, BooleanSupplier isCancelled) throws InterruptedException {
|
||||||
|
int extractedCount = 0;
|
||||||
|
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
|
if (isCancelled.getAsBoolean()) {
|
||||||
|
try {
|
||||||
|
zis.closeEntry();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to close zip entry upon cancellation", e);
|
||||||
|
}
|
||||||
|
throw new InterruptedException("Git extraction was cancelled.");
|
||||||
|
}
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
String name = entry.getName();
|
||||||
|
boolean inSubfolder = subfolder == null || subfolder.isEmpty() || name.contains("/" + subfolder + "/") || name.startsWith(subfolder + "/");
|
||||||
|
if (inSubfolder && (filterExtension == null || name.endsWith(filterExtension))) {
|
||||||
|
File outFile = new File(extractDir, new File(name).getName());
|
||||||
|
if (outFile.exists()) {
|
||||||
|
outFile = new File(extractDir, System.currentTimeMillis() + "_" + new File(name).getName());
|
||||||
|
}
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(outFile)) {
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int count;
|
||||||
|
while ((count = zis.read(buffer)) != -1) {
|
||||||
|
if (isCancelled.getAsBoolean()) {
|
||||||
|
throw new InterruptedException("Git extraction was cancelled during file read.");
|
||||||
|
}
|
||||||
|
fos.write(buffer, 0, count);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to write extracted file to disk: " + name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
zis.closeEntry();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to close zip entry", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed during unzip stream processing", e);
|
||||||
|
}
|
||||||
|
return extractedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int processDirectory(File dir, EntityImporter<Monster> importer, MonsterRepository repository, BooleanSupplier isCancelled) throws InterruptedException {
|
||||||
|
int importedCount = 0;
|
||||||
|
File[] files = dir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
Logger.logWTF("processDirectory: files array is null for " + dir.getAbsolutePath());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logWTF("Attempting to process " + files.length + " JSON files.");
|
||||||
|
|
||||||
|
for (File file : files) {
|
||||||
|
if (isCancelled.getAsBoolean()) {
|
||||||
|
throw new InterruptedException("Git import processing was cancelled.");
|
||||||
|
}
|
||||||
|
if (file.isFile()) {
|
||||||
|
String content = readFileContent(file);
|
||||||
|
if (content != null) {
|
||||||
|
if (importer.canImport(content)) {
|
||||||
|
try {
|
||||||
|
Monster monster = importer.parse(content);
|
||||||
|
repository.saveMonster(monster).blockingAwait();
|
||||||
|
importedCount++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to parse/save monster from file: " + file.getName(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.logWTF("Importer rejected file: " + file.getName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.logError("Failed to read content from file: " + file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return importedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readFileContent(File file) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
try (InputStream in = new FileInputStream(file);
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
sb.append(line).append("\n");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.logError("Error reading extracted file", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteFileOrDir(File fileOrDir) {
|
||||||
|
if (fileOrDir.isDirectory()) {
|
||||||
|
File[] children = fileOrDir.listFiles();
|
||||||
|
if (children != null) {
|
||||||
|
for (File child : children) {
|
||||||
|
deleteFileOrDir(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileOrDir.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,13 @@ public class MonsterJsonImporter implements EntityImporter<Monster> {
|
|||||||
JsonElement el = JsonParser.parseString(input);
|
JsonElement el = JsonParser.parseString(input);
|
||||||
if (el.isJsonObject()) {
|
if (el.isJsonObject()) {
|
||||||
JsonObject obj = el.getAsJsonObject();
|
JsonObject obj = el.getAsJsonObject();
|
||||||
return obj.has("name") && (obj.has("strengthScore") || obj.has("hitDice") || obj.has("size"));
|
if (obj.has("$schema")) {
|
||||||
|
String schemaVal = obj.get("$schema").getAsString();
|
||||||
|
if (schemaVal.contains("monster-card.schema.json")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj.has("name") && (obj.has("strengthScore") || obj.has("strengthSavingThrowProficiency"));
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.majinnaibu.monstercards.importers;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
|
public class Open5eApiWrapper {
|
||||||
|
private static final String BASE_URL = "https://api.open5e.com/v2/creatures/";
|
||||||
|
|
||||||
|
public static class Open5ePageResult {
|
||||||
|
public List<Monster> monsters = new ArrayList<>();
|
||||||
|
@Nullable public String nextUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Open5ePageResult fetchPage(@Nullable String urlStr, BooleanSupplier isCancelled) throws Exception {
|
||||||
|
if (urlStr == null || urlStr.isEmpty()) {
|
||||||
|
urlStr = BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL url = new URL(urlStr);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Android; Mobile)");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setConnectTimeout(15000);
|
||||||
|
conn.setReadTimeout(15000);
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code != 200) {
|
||||||
|
throw new IllegalStateException("Open5e API returned HTTP " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
try (InputStream in = conn.getInputStream();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (isCancelled.getAsBoolean()) {
|
||||||
|
throw new InterruptedException("Open5e network read was cancelled.");
|
||||||
|
}
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
} catch (InterruptedIOException e) {
|
||||||
|
throw new InterruptedException("Open5e network read was cancelled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonPayload = sb.toString();
|
||||||
|
JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();
|
||||||
|
|
||||||
|
Open5ePageResult result = new Open5ePageResult();
|
||||||
|
if (root.has("next") && !root.get("next").isJsonNull()) {
|
||||||
|
result.nextUrl = root.get("next").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.has("results") && root.get("results").isJsonArray()) {
|
||||||
|
JsonArray results = root.getAsJsonArray("results");
|
||||||
|
Open5eImporter importer = new Open5eImporter();
|
||||||
|
for (int i = 0; i < results.size(); i++) {
|
||||||
|
JsonElement el = results.get(i);
|
||||||
|
if (el.isJsonObject()) {
|
||||||
|
try {
|
||||||
|
Monster monster = importer.parse(el.toString());
|
||||||
|
result.monsters.add(monster);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to parse Open5e monster", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,16 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
parseArmorClass(props, monster);
|
parseArmorClass(props, monster);
|
||||||
parseHitPoints(props, monster);
|
parseHitPoints(props, monster);
|
||||||
|
|
||||||
monster.walkSpeed = getInt(props, "speed", 30);
|
if (props.has("speed") && props.get("speed").isJsonObject()) {
|
||||||
|
JsonObject speedObj = props.getAsJsonObject("speed");
|
||||||
|
monster.walkSpeed = getInt(speedObj, "walk", 30);
|
||||||
|
monster.flySpeed = getInt(speedObj, "fly", 0);
|
||||||
|
monster.swimSpeed = getInt(speedObj, "swim", 0);
|
||||||
|
monster.burrowSpeed = getInt(speedObj, "burrow", 0);
|
||||||
|
monster.climbSpeed = getInt(speedObj, "climb", 0);
|
||||||
|
} else {
|
||||||
|
monster.walkSpeed = getInt(props, "speed", 30);
|
||||||
|
}
|
||||||
if (props.has("speed_desc")) {
|
if (props.has("speed_desc")) {
|
||||||
String speedDesc = getString(props, "speed_desc");
|
String speedDesc = getString(props, "speed_desc");
|
||||||
if (!speedDesc.isEmpty()) {
|
if (!speedDesc.isEmpty()) {
|
||||||
@@ -159,6 +168,10 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
crStr = props.get("challenge_rating").getAsString();
|
crStr = props.get("challenge_rating").getAsString();
|
||||||
}
|
}
|
||||||
if (!crStr.isEmpty()) {
|
if (!crStr.isEmpty()) {
|
||||||
|
crStr = crStr.replace(".0", "");
|
||||||
|
if (crStr.equals("0.125")) crStr = "1/8";
|
||||||
|
else if (crStr.equals("0.25")) crStr = "1/4";
|
||||||
|
else if (crStr.equals("0.5")) crStr = "1/2";
|
||||||
monster.challengeRating = ChallengeRating.valueOfString(crStr);
|
monster.challengeRating = ChallengeRating.valueOfString(crStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,7 +227,12 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
private void parseSavingThrows(JsonObject props, Monster monster) {
|
private void parseSavingThrows(JsonObject props, Monster monster) {
|
||||||
if (!props.has("saving_throws")) return;
|
if (!props.has("saving_throws")) return;
|
||||||
JsonElement savesEl = props.get("saving_throws");
|
JsonElement savesEl = props.get("saving_throws");
|
||||||
if (savesEl.isJsonArray()) {
|
if (savesEl.isJsonObject()) {
|
||||||
|
JsonObject savesObj = savesEl.getAsJsonObject();
|
||||||
|
for (String key : savesObj.keySet()) {
|
||||||
|
applySaveProficiency(monster, key.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
} else if (savesEl.isJsonArray()) {
|
||||||
JsonArray arr = savesEl.getAsJsonArray();
|
JsonArray arr = savesEl.getAsJsonArray();
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
String saveStr = arr.get(i).getAsString().toLowerCase(Locale.ROOT);
|
String saveStr = arr.get(i).getAsString().toLowerCase(Locale.ROOT);
|
||||||
@@ -233,16 +251,23 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void parseSkills(JsonObject props, Monster monster) {
|
private void parseSkills(JsonObject props, Monster monster) {
|
||||||
if (!props.has("skills")) return;
|
if (props.has("skill_bonuses") && props.get("skill_bonuses").isJsonObject()) {
|
||||||
JsonElement skillsEl = props.get("skills");
|
JsonObject skillsObj = props.getAsJsonObject("skill_bonuses");
|
||||||
if (skillsEl.isJsonArray()) {
|
for (String key : skillsObj.keySet()) {
|
||||||
JsonArray arr = skillsEl.getAsJsonArray();
|
AbilityScore stat = getAbilityForSkill(key);
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
monster.skills.add(new Skill(capitalize(key.replace("_", " ")), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
|
||||||
String skillStr = arr.get(i).getAsString();
|
}
|
||||||
String skillName = skillStr.replaceAll("[+-]\\d+", "").trim();
|
} else if (props.has("skills")) {
|
||||||
if (!skillName.isEmpty()) {
|
JsonElement skillsEl = props.get("skills");
|
||||||
AbilityScore stat = getAbilityForSkill(skillName);
|
if (skillsEl.isJsonArray()) {
|
||||||
monster.skills.add(new Skill(capitalize(skillName), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
|
JsonArray arr = skillsEl.getAsJsonArray();
|
||||||
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
|
String skillStr = arr.get(i).getAsString();
|
||||||
|
String skillName = skillStr.replaceAll("[+-]\\d+", "").trim();
|
||||||
|
if (!skillName.isEmpty()) {
|
||||||
|
AbilityScore stat = getAbilityForSkill(skillName);
|
||||||
|
monster.skills.add(new Skill(capitalize(skillName), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +299,26 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
private void parseLanguages(JsonObject props, Monster monster) {
|
private void parseLanguages(JsonObject props, Monster monster) {
|
||||||
if (props.has("languages")) {
|
if (props.has("languages")) {
|
||||||
JsonElement langEl = props.get("languages");
|
JsonElement langEl = props.get("languages");
|
||||||
if (langEl.isJsonArray()) {
|
if (langEl.isJsonObject()) {
|
||||||
|
if (langEl.getAsJsonObject().has("data") && langEl.getAsJsonObject().get("data").isJsonArray()) {
|
||||||
|
JsonArray arr = langEl.getAsJsonObject().getAsJsonArray("data");
|
||||||
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
|
if (arr.get(i).isJsonObject()) {
|
||||||
|
String langName = getString(arr.get(i).getAsJsonObject(), "name");
|
||||||
|
if (!langName.isEmpty()) {
|
||||||
|
monster.languages.add(new Language(langName, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (langEl.getAsJsonObject().has("as_string")) {
|
||||||
|
String langStr = langEl.getAsJsonObject().get("as_string").getAsString();
|
||||||
|
for (String l : langStr.split(",")) {
|
||||||
|
if (!l.trim().isEmpty()) {
|
||||||
|
monster.languages.add(new Language(l.trim(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (langEl.isJsonArray()) {
|
||||||
JsonArray arr = langEl.getAsJsonArray();
|
JsonArray arr = langEl.getAsJsonArray();
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
String langName = arr.get(i).getAsString();
|
String langName = arr.get(i).getAsString();
|
||||||
@@ -294,10 +338,18 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void parseDamageAndConditions(JsonObject props, Monster monster) {
|
private void parseDamageAndConditions(JsonObject props, Monster monster) {
|
||||||
parseStringSet(props, "damage_immunities", monster.damageImmunities);
|
if (props.has("resistances_and_immunities") && props.get("resistances_and_immunities").isJsonObject()) {
|
||||||
parseStringSet(props, "damage_resistances", monster.damageResistances);
|
JsonObject resObj = props.getAsJsonObject("resistances_and_immunities");
|
||||||
parseStringSet(props, "damage_vulnerabilities", monster.damageVulnerabilities);
|
parseStringSet(resObj, "damage_immunities", monster.damageImmunities);
|
||||||
parseStringSet(props, "condition_immunities", monster.conditionImmunities);
|
parseStringSet(resObj, "damage_resistances", monster.damageResistances);
|
||||||
|
parseStringSet(resObj, "damage_vulnerabilities", monster.damageVulnerabilities);
|
||||||
|
parseStringSet(resObj, "condition_immunities", monster.conditionImmunities);
|
||||||
|
} else {
|
||||||
|
parseStringSet(props, "damage_immunities", monster.damageImmunities);
|
||||||
|
parseStringSet(props, "damage_resistances", monster.damageResistances);
|
||||||
|
parseStringSet(props, "damage_vulnerabilities", monster.damageVulnerabilities);
|
||||||
|
parseStringSet(props, "condition_immunities", monster.conditionImmunities);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseStringSet(JsonObject props, String key, java.util.Set<String> set) {
|
private void parseStringSet(JsonObject props, String key, java.util.Set<String> set) {
|
||||||
@@ -306,8 +358,14 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
if (el.isJsonArray()) {
|
if (el.isJsonArray()) {
|
||||||
JsonArray arr = el.getAsJsonArray();
|
JsonArray arr = el.getAsJsonArray();
|
||||||
for (int i = 0; i < arr.size(); i++) {
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
String val = arr.get(i).getAsString();
|
JsonElement item = arr.get(i);
|
||||||
if (!val.isEmpty()) set.add(capitalize(val));
|
if (item.isJsonObject() && item.getAsJsonObject().has("name")) {
|
||||||
|
String val = item.getAsJsonObject().get("name").getAsString();
|
||||||
|
if (!val.isEmpty()) set.add(capitalize(val));
|
||||||
|
} else if (item.isJsonPrimitive()) {
|
||||||
|
String val = item.getAsString();
|
||||||
|
if (!val.isEmpty()) set.add(capitalize(val));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (el.isJsonPrimitive()) {
|
} else if (el.isJsonPrimitive()) {
|
||||||
for (String val : el.getAsString().split(",")) {
|
for (String val : el.getAsString().split(",")) {
|
||||||
@@ -357,7 +415,12 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
|||||||
|
|
||||||
private static String getString(JsonObject obj, String key, String defaultVal) {
|
private static String getString(JsonObject obj, String key, String defaultVal) {
|
||||||
if (obj.has(key) && !obj.get(key).isJsonNull()) {
|
if (obj.has(key) && !obj.get(key).isJsonNull()) {
|
||||||
return obj.get(key).getAsString();
|
JsonElement el = obj.get(key);
|
||||||
|
if (el.isJsonPrimitive()) {
|
||||||
|
return el.getAsString();
|
||||||
|
} else if (el.isJsonObject() && el.getAsJsonObject().has("name")) {
|
||||||
|
return el.getAsJsonObject().get("name").getAsString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return defaultVal;
|
return defaultVal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package com.majinnaibu.monstercards.importers;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.AdvantageType;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.ArmorType;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.ChallengeRating;
|
||||||
|
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
||||||
|
import com.majinnaibu.monstercards.models.Language;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
import com.majinnaibu.monstercards.models.Skill;
|
||||||
|
import com.majinnaibu.monstercards.models.Trait;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Pf2eImporter implements EntityImporter<Monster> {
|
||||||
|
@Override
|
||||||
|
public boolean canImport(@NonNull String input) {
|
||||||
|
String trimmed = input.trim();
|
||||||
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonObject root = JsonParser.parseString(trimmed).getAsJsonObject();
|
||||||
|
if (root.has("type") && "npc".equals(root.get("type").getAsString())) {
|
||||||
|
if (root.has("system") && root.getAsJsonObject("system").has("attributes")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Monster parse(@NonNull String input) throws Exception {
|
||||||
|
JsonObject root = JsonParser.parseString(input.trim()).getAsJsonObject();
|
||||||
|
JsonObject system = root.has("system") ? root.getAsJsonObject("system") : new JsonObject();
|
||||||
|
|
||||||
|
Monster monster = new Monster();
|
||||||
|
monster.id = UUID.randomUUID();
|
||||||
|
|
||||||
|
// Name
|
||||||
|
if (root.has("name") && !root.get("name").isJsonNull()) {
|
||||||
|
monster.name = root.get("name").getAsString();
|
||||||
|
} else {
|
||||||
|
monster.name = "Unknown PF2e Monster";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type / Size / Alignment
|
||||||
|
if (system.has("traits") && system.get("traits").isJsonObject()) {
|
||||||
|
JsonObject traits = system.getAsJsonObject("traits");
|
||||||
|
if (traits.has("size") && traits.get("size").isJsonObject()) {
|
||||||
|
monster.size = mapPf2eSize(getString(traits.getAsJsonObject("size"), "value"));
|
||||||
|
}
|
||||||
|
if (traits.has("value") && traits.get("value").isJsonArray()) {
|
||||||
|
// PF2e holds type/alignment tags in traits array
|
||||||
|
StringBuilder typeSb = new StringBuilder();
|
||||||
|
for (JsonElement el : traits.getAsJsonArray("value")) {
|
||||||
|
String trait = el.getAsString();
|
||||||
|
if (typeSb.length() > 0) typeSb.append(", ");
|
||||||
|
typeSb.append(capitalize(trait));
|
||||||
|
}
|
||||||
|
monster.type = typeSb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ability Scores
|
||||||
|
if (system.has("abilities") && system.get("abilities").isJsonObject()) {
|
||||||
|
JsonObject abilities = system.getAsJsonObject("abilities");
|
||||||
|
monster.strengthScore = 10 + (getInt(abilities.getAsJsonObject("str"), "mod") * 2);
|
||||||
|
monster.dexterityScore = 10 + (getInt(abilities.getAsJsonObject("dex"), "mod") * 2);
|
||||||
|
monster.constitutionScore = 10 + (getInt(abilities.getAsJsonObject("con"), "mod") * 2);
|
||||||
|
monster.intelligenceScore = 10 + (getInt(abilities.getAsJsonObject("int"), "mod") * 2);
|
||||||
|
monster.wisdomScore = 10 + (getInt(abilities.getAsJsonObject("wis"), "mod") * 2);
|
||||||
|
monster.charismaScore = 10 + (getInt(abilities.getAsJsonObject("cha"), "mod") * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes (HP, AC, Speed, Level)
|
||||||
|
if (system.has("attributes") && system.get("attributes").isJsonObject()) {
|
||||||
|
JsonObject attributes = system.getAsJsonObject("attributes");
|
||||||
|
|
||||||
|
if (attributes.has("ac") && attributes.get("ac").isJsonObject()) {
|
||||||
|
monster.armorType = ArmorType.NONE;
|
||||||
|
monster.otherArmorDescription = String.valueOf(getInt(attributes.getAsJsonObject("ac"), "value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.has("hp") && attributes.get("hp").isJsonObject()) {
|
||||||
|
monster.hasCustomHP = true;
|
||||||
|
monster.customHPDescription = String.valueOf(getInt(attributes.getAsJsonObject("hp"), "max"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.has("speed") && attributes.get("speed").isJsonObject()) {
|
||||||
|
monster.walkSpeed = getInt(attributes.getAsJsonObject("speed"), "value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level (Mapped to CR)
|
||||||
|
if (system.has("details") && system.get("details").isJsonObject()) {
|
||||||
|
JsonObject details = system.getAsJsonObject("details");
|
||||||
|
if (details.has("level") && details.get("level").isJsonObject()) {
|
||||||
|
int level = getInt(details.getAsJsonObject("level"), "value");
|
||||||
|
monster.challengeRating = mapLevelToCr(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.has("languages") && details.get("languages").isJsonObject()) {
|
||||||
|
JsonObject langObj = details.getAsJsonObject("languages");
|
||||||
|
if (langObj.has("value") && langObj.get("value").isJsonArray()) {
|
||||||
|
for (JsonElement el : langObj.getAsJsonArray("value")) {
|
||||||
|
monster.languages.add(new Language(capitalize(el.getAsString()), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.has("publicNotes") && !details.get("publicNotes").isJsonNull()) {
|
||||||
|
String notes = details.get("publicNotes").getAsString().replaceAll("<[^>]*>", "").trim();
|
||||||
|
monster.backstory = notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return monster;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mapPf2eSize(String pf2eSize) {
|
||||||
|
if (pf2eSize == null) return "Medium";
|
||||||
|
switch (pf2eSize.toLowerCase(Locale.ROOT)) {
|
||||||
|
case "tiny": return "Tiny";
|
||||||
|
case "sm": return "Small";
|
||||||
|
case "med": return "Medium";
|
||||||
|
case "lg": return "Large";
|
||||||
|
case "huge": return "Huge";
|
||||||
|
case "grg": return "Gargantuan";
|
||||||
|
default: return "Medium";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChallengeRating mapLevelToCr(int level) {
|
||||||
|
if (level <= 0) return ChallengeRating.ZERO;
|
||||||
|
if (level > 30) return ChallengeRating.THIRTY;
|
||||||
|
return ChallengeRating.valueOfString(String.valueOf(level));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getString(JsonObject obj, String key) {
|
||||||
|
if (obj != null && obj.has(key) && !obj.get(key).isJsonNull()) {
|
||||||
|
return obj.get(key).getAsString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getInt(JsonObject obj, String key) {
|
||||||
|
if (obj != null && obj.has(key) && !obj.get(key).isJsonNull()) {
|
||||||
|
try {
|
||||||
|
return obj.get(key).getAsInt();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String capitalize(String str) {
|
||||||
|
if (str == null || str.isEmpty()) return str;
|
||||||
|
return str.substring(0, 1).toUpperCase(Locale.ROOT) + str.substring(1).toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,12 @@ public class TetraCubeMonsterImporter implements EntityImporter<Monster> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
JsonObject rootDict = element.getAsJsonObject();
|
JsonObject rootDict = element.getAsJsonObject();
|
||||||
|
if (rootDict.has("$schema")) {
|
||||||
|
String schemaVal = rootDict.get("$schema").getAsString();
|
||||||
|
if (schemaVal.contains("tetracube-monster.schema.json")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return rootDict.has("armorName") || rootDict.has("strPoints")
|
return rootDict.has("armorName") || rootDict.has("strPoints")
|
||||||
|| rootDict.has("isLegendary") || rootDict.has("isMythic")
|
|| rootDict.has("isLegendary") || rootDict.has("isMythic")
|
||||||
|| rootDict.has("legendariesDescription");
|
|| rootDict.has("legendariesDescription");
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.majinnaibu.monstercards.models;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class BinderExport {
|
||||||
|
@SerializedName("$schema")
|
||||||
|
public String schema = "https://majinnaibu.com/schemas/binder.schema.json";
|
||||||
|
|
||||||
|
@SerializedName("schemaVersion")
|
||||||
|
public int schemaVersion = 1;
|
||||||
|
|
||||||
|
@SerializedName("collections")
|
||||||
|
public List<CollectionExport> collections = new ArrayList<>();
|
||||||
|
|
||||||
|
@SerializedName("dashboard")
|
||||||
|
public List<Monster> dashboard = new ArrayList<>();
|
||||||
|
|
||||||
|
public static class CollectionExport {
|
||||||
|
@SerializedName("id")
|
||||||
|
public String id;
|
||||||
|
|
||||||
|
@SerializedName("name")
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
@SerializedName("description")
|
||||||
|
public String description;
|
||||||
|
|
||||||
|
@SerializedName("cards")
|
||||||
|
public List<Monster> cards = new ArrayList<>();
|
||||||
|
|
||||||
|
public CollectionExport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public CollectionExport(String id, String name, String description, List<Monster> cards) {
|
||||||
|
this.id = id != null ? id : UUID.randomUUID().toString();
|
||||||
|
this.name = name != null ? name : "";
|
||||||
|
this.description = description != null ? description : "";
|
||||||
|
this.cards = cards != null ? cards : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CollectionExport(String name, List<Monster> cards) {
|
||||||
|
this(UUID.randomUUID().toString(), name, "", cards);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.Ignore;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -30,6 +31,7 @@ public class Collection {
|
|||||||
this.description = "";
|
this.description = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
public Collection(@NonNull String name, @NonNull String description) {
|
public Collection(@NonNull String name, @NonNull String description) {
|
||||||
this.id = UUID.randomUUID();
|
this.id = UUID.randomUUID();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.ForeignKey;
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Ignore;
|
||||||
import androidx.room.Index;
|
import androidx.room.Index;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ public class CollectionMonster {
|
|||||||
this.ordinal = 0;
|
this.ordinal = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
public CollectionMonster(@NonNull UUID collectionId, @NonNull UUID monsterId, int ordinal) {
|
public CollectionMonster(@NonNull UUID collectionId, @NonNull UUID monsterId, int ordinal) {
|
||||||
this.collectionId = collectionId;
|
this.collectionId = collectionId;
|
||||||
this.monsterId = monsterId;
|
this.monsterId = monsterId;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.majinnaibu.monstercards.models;
|
package com.majinnaibu.monstercards.models;
|
||||||
|
|
||||||
import androidx.room.Embedded;
|
import androidx.room.Embedded;
|
||||||
|
import androidx.room.Ignore;
|
||||||
|
|
||||||
public class CollectionWithCount {
|
public class CollectionWithCount {
|
||||||
@Embedded
|
@Embedded
|
||||||
@@ -13,6 +14,7 @@ public class CollectionWithCount {
|
|||||||
this.monsterCount = 0;
|
this.monsterCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
public CollectionWithCount(Collection collection, int monsterCount) {
|
public CollectionWithCount(Collection collection, int monsterCount) {
|
||||||
this.collection = collection;
|
this.collection = collection;
|
||||||
this.monsterCount = monsterCount;
|
this.monsterCount = monsterCount;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.ForeignKey;
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Ignore;
|
||||||
import androidx.room.Index;
|
import androidx.room.Index;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ public class DashboardMonster {
|
|||||||
this.ordinal = 0;
|
this.ordinal = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
public DashboardMonster(@NonNull UUID monsterId, int ordinal) {
|
public DashboardMonster(@NonNull UUID monsterId, int ordinal) {
|
||||||
this.monsterId = monsterId;
|
this.monsterId = monsterId;
|
||||||
this.ordinal = ordinal;
|
this.ordinal = ordinal;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.majinnaibu.monstercards.models;
|
||||||
|
|
||||||
|
import androidx.room.Embedded;
|
||||||
|
import androidx.room.Ignore;
|
||||||
|
import androidx.room.Relation;
|
||||||
|
|
||||||
|
public class DashboardMonsterWithMonster {
|
||||||
|
@Embedded
|
||||||
|
public DashboardMonster dashboardEntry;
|
||||||
|
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "monster_id",
|
||||||
|
entityColumn = "id"
|
||||||
|
)
|
||||||
|
public Monster monster;
|
||||||
|
|
||||||
|
public DashboardMonsterWithMonster() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public DashboardMonsterWithMonster(DashboardMonster dashboardEntry, Monster monster) {
|
||||||
|
this.dashboardEntry = dashboardEntry;
|
||||||
|
this.monster = monster;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.majinnaibu.monstercards.models;
|
||||||
|
|
||||||
|
public class ImportSource {
|
||||||
|
public enum ImportType {
|
||||||
|
GIT_ARCHIVE,
|
||||||
|
OPEN5E_API
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id;
|
||||||
|
public String projectName;
|
||||||
|
public String creatorName;
|
||||||
|
public String creatorPageLink;
|
||||||
|
public ImportType importType;
|
||||||
|
|
||||||
|
// Optional attributes specific to Git archive
|
||||||
|
public String downloadUrl;
|
||||||
|
public String importerClassName;
|
||||||
|
public String fileExtension;
|
||||||
|
public String subfolder;
|
||||||
|
|
||||||
|
public ImportSource(String id, String projectName, String creatorName, String creatorPageLink, ImportType importType, String downloadUrl, String importerClassName, String fileExtension, String subfolder) {
|
||||||
|
this.id = id;
|
||||||
|
this.projectName = projectName;
|
||||||
|
this.creatorName = creatorName;
|
||||||
|
this.creatorPageLink = creatorPageLink;
|
||||||
|
this.importType = importType;
|
||||||
|
this.downloadUrl = downloadUrl;
|
||||||
|
this.importerClassName = importerClassName;
|
||||||
|
this.fileExtension = fileExtension;
|
||||||
|
this.subfolder = subfolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.Ignore;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
||||||
import com.majinnaibu.monstercards.data.enums.AdvantageType;
|
import com.majinnaibu.monstercards.data.enums.AdvantageType;
|
||||||
import com.majinnaibu.monstercards.data.enums.ArmorType;
|
import com.majinnaibu.monstercards.data.enums.ArmorType;
|
||||||
@@ -29,8 +31,16 @@ import java.util.UUID;
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class Monster {
|
public class Monster {
|
||||||
|
|
||||||
|
@SerializedName("$schema")
|
||||||
|
@Ignore
|
||||||
|
public String schema = "https://majinnaibu.com/schemas/monster-card.schema.json";
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public int schemaVersion = 1;
|
||||||
|
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@SerializedName("id")
|
||||||
public UUID id;
|
public UUID id;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -434,7 +444,10 @@ public class Monster {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAbilityModifier(@NonNull AbilityScore abilityScore) {
|
public int getAbilityModifier(@Nullable AbilityScore abilityScore) {
|
||||||
|
if (abilityScore == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
switch (abilityScore) {
|
switch (abilityScore) {
|
||||||
case STRENGTH:
|
case STRENGTH:
|
||||||
return getStrengthModifier();
|
return getStrengthModifier();
|
||||||
|
|||||||
@@ -34,13 +34,25 @@ public class Skill implements Comparator<Skill>, Comparable<Skill> {
|
|||||||
this.proficiencyType = proficiencyType;
|
this.proficiencyType = proficiencyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AbilityScore getAbilityScore() {
|
||||||
|
if (abilityScore != null) {
|
||||||
|
return abilityScore;
|
||||||
|
}
|
||||||
|
if (name != null) {
|
||||||
|
return AbilityScore.valueOfString(name);
|
||||||
|
}
|
||||||
|
return AbilityScore.STRENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
public int getSkillBonus(Monster monster) {
|
public int getSkillBonus(Monster monster) {
|
||||||
int modifier = monster.getAbilityModifier(abilityScore);
|
AbilityScore score = getAbilityScore();
|
||||||
switch (proficiencyType) {
|
int modifier = monster != null ? monster.getAbilityModifier(score) : 0;
|
||||||
|
ProficiencyType prof = proficiencyType != null ? proficiencyType : ProficiencyType.PROFICIENT;
|
||||||
|
switch (prof) {
|
||||||
case PROFICIENT:
|
case PROFICIENT:
|
||||||
return modifier + monster.getProficiencyBonus();
|
return modifier + (monster != null ? monster.getProficiencyBonus() : 0);
|
||||||
case EXPERTISE:
|
case EXPERTISE:
|
||||||
return modifier + monster.getProficiencyBonus() * 2;
|
return modifier + (monster != null ? monster.getProficiencyBonus() * 2 : 0);
|
||||||
case NONE:
|
case NONE:
|
||||||
default:
|
default:
|
||||||
return modifier;
|
return modifier;
|
||||||
|
|||||||
@@ -21,15 +21,18 @@ import androidx.recyclerview.widget.GridLayoutManager;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
import com.majinnaibu.monstercards.models.Collection;
|
||||||
|
import com.majinnaibu.monstercards.models.DashboardMonster;
|
||||||
|
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.ui.dashboard.DashboardRecyclerViewAdapter;
|
import com.majinnaibu.monstercards.ui.dashboard.DashboardRecyclerViewAdapter;
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
FloatingActionButton fab = root.findViewById(R.id.fab_add_monster_to_collection);
|
FloatingActionButton fab = root.findViewById(R.id.fab_add_monster_to_collection);
|
||||||
|
|
||||||
if (fab != null) {
|
if (fab != null) {
|
||||||
fab.setOnClickListener(v -> showAddMonsterDialog());
|
fab.setOnClickListener(v -> showCollectionDetailFabOptionsDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
mViewModel.getCollection().observe(getViewLifecycleOwner(), collection -> {
|
mViewModel.getCollection().observe(getViewLifecycleOwner(), collection -> {
|
||||||
@@ -80,7 +83,15 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
|
|
||||||
mViewModel.getMonsters().observe(getViewLifecycleOwner(), monsters -> {
|
mViewModel.getMonsters().observe(getViewLifecycleOwner(), monsters -> {
|
||||||
if (mAdapter != null) {
|
if (mAdapter != null) {
|
||||||
mAdapter.submitList(monsters);
|
List<DashboardMonsterWithMonster> items = new ArrayList<>();
|
||||||
|
if (monsters != null) {
|
||||||
|
for (int i = 0; i < monsters.size(); i++) {
|
||||||
|
DashboardMonster dm = new DashboardMonster(monsters.get(i).id, i);
|
||||||
|
dm.id = i + 1;
|
||||||
|
items.add(new DashboardMonsterWithMonster(dm, monsters.get(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mAdapter.submitList(items);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,9 +124,9 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
|
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
|
|
||||||
mAdapter = new DashboardRecyclerViewAdapter(monster -> {
|
mAdapter = new DashboardRecyclerViewAdapter(item -> {
|
||||||
if (monster != null) {
|
if (item != null && item.monster != null) {
|
||||||
navigateToMonsterDetail(monster.id);
|
navigateToMonsterDetail(item.monster.id);
|
||||||
} else {
|
} else {
|
||||||
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
|
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
|
||||||
}
|
}
|
||||||
@@ -123,6 +134,27 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
recyclerView.setAdapter(mAdapter);
|
recyclerView.setAdapter(mAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showCollectionDetailFabOptionsDialog() {
|
||||||
|
String[] options = new String[]{
|
||||||
|
getString(R.string.action_add_monster),
|
||||||
|
getString(R.string.action_create_monster),
|
||||||
|
getString(R.string.action_import)
|
||||||
|
};
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.title_collection_actions)
|
||||||
|
.setItems(options, (dialog, which) -> {
|
||||||
|
if (which == 0) {
|
||||||
|
showAddMonsterDialog();
|
||||||
|
} else if (which == 1) {
|
||||||
|
createNewMonster();
|
||||||
|
} else if (which == 2) {
|
||||||
|
showImportDialog();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
private void showAddMonsterDialog() {
|
private void showAddMonsterDialog() {
|
||||||
MonsterRepository repository = getMonsterRepository();
|
MonsterRepository repository = getMonsterRepository();
|
||||||
mDisposables.add(repository.getMonsters()
|
mDisposables.add(repository.getMonsters()
|
||||||
@@ -133,7 +165,7 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
if (monsters.isEmpty()) {
|
if (monsters.isEmpty()) {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,11 +194,7 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
if (view != null) {
|
if (view != null) {
|
||||||
Collection collection = mViewModel.getCollection().getValue();
|
Collection collection = mViewModel.getCollection().getValue();
|
||||||
String collectionName = collection != null ? collection.name : "";
|
String collectionName = collection != null ? collection.name : "";
|
||||||
Snackbar.make(
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName));
|
||||||
view,
|
|
||||||
getString(R.string.snackbar_monster_added_to_collection, monster.name, collectionName),
|
|
||||||
Snackbar.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
}, Logger::logError));
|
}, Logger::logError));
|
||||||
}
|
}
|
||||||
@@ -187,10 +215,50 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
|
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
|
||||||
addCollectionToDashboard();
|
addCollectionToDashboard();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_collection) {
|
||||||
|
exportCollection();
|
||||||
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_remove_all_monsters_from_collection) {
|
||||||
|
showRemoveAllMonstersConfirmationDialog();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showRemoveAllMonstersConfirmationDialog() {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.dialog_remove_all_from_collection_title)
|
||||||
|
.setMessage(R.string.dialog_remove_all_from_collection_message)
|
||||||
|
.setPositiveButton(R.string.action_remove_all, (dialog, which) -> {
|
||||||
|
mDisposables.add(getMonsterRepository().removeAllMonstersFromCollection(mCollectionId)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.showLong(view, R.string.snackbar_collection_cleared);
|
||||||
|
}
|
||||||
|
}, throwable -> Logger.logError("Failed to remove all monsters from collection", throwable)));
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportCollection() {
|
||||||
|
Collection collection = mViewModel.getCollection().getValue();
|
||||||
|
List<Monster> monsters = mViewModel.getMonsters().getValue();
|
||||||
|
if (collection != null) {
|
||||||
|
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder(
|
||||||
|
collection.name,
|
||||||
|
monsters != null ? monsters : new java.util.ArrayList<>()
|
||||||
|
);
|
||||||
|
String collectionName = (collection.name != null && !collection.name.trim().isEmpty())
|
||||||
|
? collection.name.trim()
|
||||||
|
: "collection";
|
||||||
|
exportToFile(collectionName + ".binder", json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addCollectionToDashboard() {
|
private void addCollectionToDashboard() {
|
||||||
MonsterRepository repository = getMonsterRepository();
|
MonsterRepository repository = getMonsterRepository();
|
||||||
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
|
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
|
||||||
@@ -201,7 +269,7 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
if (view != null) {
|
if (view != null) {
|
||||||
Collection collection = mViewModel.getCollection().getValue();
|
Collection collection = mViewModel.getCollection().getValue();
|
||||||
String collectionName = collection != null ? collection.name : "";
|
String collectionName = collection != null ? collection.name : "";
|
||||||
Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collectionName), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_collection_added_to_dashboard, collectionName));
|
||||||
}
|
}
|
||||||
}, Logger::logError));
|
}, Logger::logError));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import android.content.Context;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
@@ -20,46 +23,66 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
import com.majinnaibu.monstercards.models.Collection;
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
|
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
|
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class CollectionsFragment extends MCFragment {
|
public class CollectionsFragment extends MCFragment {
|
||||||
|
private final CompositeDisposable mDisposables = new CompositeDisposable();
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
@Nullable ViewGroup container,
|
@Nullable ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
setHasOptionsMenu(true);
|
||||||
View root = inflater.inflate(R.layout.fragment_collections, container, false);
|
View root = inflater.inflate(R.layout.fragment_collections, container, false);
|
||||||
|
|
||||||
FloatingActionButton fab = root.findViewById(R.id.fab_add_collection);
|
FloatingActionButton fab = root.findViewById(R.id.fab_add_collection);
|
||||||
if (fab != null) {
|
if (fab != null) {
|
||||||
fab.setOnClickListener(v -> showCreateCollectionDialog());
|
fab.setOnClickListener(v -> showCollectionFabOptionsDialog());
|
||||||
|
}
|
||||||
|
|
||||||
|
View emptyState = root.findViewById(R.id.empty_state);
|
||||||
|
View emptyStateButton = root.findViewById(R.id.empty_state_button);
|
||||||
|
if (emptyStateButton != null) {
|
||||||
|
emptyStateButton.setOnClickListener(v -> showCollectionFabOptionsDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
RecyclerView recyclerView = root.findViewById(R.id.collection_list);
|
RecyclerView recyclerView = root.findViewById(R.id.collection_list);
|
||||||
if (recyclerView != null) {
|
if (recyclerView != null) {
|
||||||
setupRecyclerView(recyclerView);
|
setupRecyclerView(recyclerView, emptyState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) {
|
||||||
Context context = requireContext();
|
Context context = requireContext();
|
||||||
MonsterRepository repository = getMonsterRepository();
|
MonsterRepository repository = getMonsterRepository();
|
||||||
|
|
||||||
|
mDisposables.add(repository.getCollectionsWithCount()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(collections -> {
|
||||||
|
boolean isEmpty = (collections == null || collections.isEmpty());
|
||||||
|
if (emptyState != null) {
|
||||||
|
emptyState.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
recyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
|
||||||
|
}, Logger::logError));
|
||||||
|
|
||||||
CollectionsRecyclerViewAdapter adapter = new CollectionsRecyclerViewAdapter(
|
CollectionsRecyclerViewAdapter adapter = new CollectionsRecyclerViewAdapter(
|
||||||
context,
|
context,
|
||||||
repository.getCollectionsWithCount(),
|
repository.getCollectionsWithCount(),
|
||||||
@@ -90,6 +113,24 @@ public class CollectionsFragment extends MCFragment {
|
|||||||
itemTouchHelper.attachToRecyclerView(recyclerView);
|
itemTouchHelper.attachToRecyclerView(recyclerView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showCollectionFabOptionsDialog() {
|
||||||
|
String[] options = new String[]{
|
||||||
|
getString(R.string.title_new_collection),
|
||||||
|
getString(R.string.action_import_collection)
|
||||||
|
};
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.title_collection_actions)
|
||||||
|
.setItems(options, (dialog, which) -> {
|
||||||
|
if (which == 0) {
|
||||||
|
showCreateCollectionDialog();
|
||||||
|
} else if (which == 1) {
|
||||||
|
importCollectionFromFile();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
private void showCreateCollectionDialog() {
|
private void showCreateCollectionDialog() {
|
||||||
Context context = requireContext();
|
Context context = requireContext();
|
||||||
LinearLayout layout = new LinearLayout(context);
|
LinearLayout layout = new LinearLayout(context);
|
||||||
@@ -131,10 +172,7 @@ public class CollectionsFragment extends MCFragment {
|
|||||||
public void onComplete() {
|
public void onComplete() {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(
|
SnackbarHelper.makeLong(view, getString(R.string.snackbar_collection_created, collection.name))
|
||||||
view,
|
|
||||||
getString(R.string.snackbar_collection_created, collection.name),
|
|
||||||
Snackbar.LENGTH_LONG)
|
|
||||||
.setAction("View", v -> navigateToCollectionDetail(collection.id))
|
.setAction("View", v -> navigateToCollectionDetail(collection.id))
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
@@ -145,7 +183,7 @@ public class CollectionsFragment extends MCFragment {
|
|||||||
Logger.logError("Error creating collection", e);
|
Logger.logError("Error creating collection", e);
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_collection), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_collection));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -155,4 +193,55 @@ public class CollectionsFragment extends MCFragment {
|
|||||||
NavDirections action = CollectionsFragmentDirections.actionNavigationCollectionsToCollectionDetailFragment(collectionId.toString());
|
NavDirections action = CollectionsFragmentDirections.actionNavigationCollectionsToCollectionDetailFragment(collectionId.toString());
|
||||||
Navigation.findNavController(requireView()).navigate(action);
|
Navigation.findNavController(requireView()).navigate(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||||
|
inflater.inflate(R.menu.collections_menu, menu);
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.menu_action_export_collections) {
|
||||||
|
exportCollections();
|
||||||
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_remove_all_collections) {
|
||||||
|
showRemoveAllCollectionsConfirmationDialog();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportCollections() {
|
||||||
|
mDisposables.add(getMonsterRepository().exportCollections()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(json -> exportToFile("collections.binder", json),
|
||||||
|
throwable -> Logger.logError("Failed to export collections", throwable)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showRemoveAllCollectionsConfirmationDialog() {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.dialog_remove_all_collections_title)
|
||||||
|
.setMessage(R.string.dialog_remove_all_collections_message)
|
||||||
|
.setPositiveButton(R.string.action_remove_all, (dialog, which) -> {
|
||||||
|
mDisposables.add(getMonsterRepository().removeAllCollections()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.showLong(view, R.string.snackbar_all_collections_removed);
|
||||||
|
}
|
||||||
|
}, throwable -> Logger.logError("Failed to remove all collections", throwable)));
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
mDisposables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
import com.majinnaibu.monstercards.models.Collection;
|
||||||
|
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -57,6 +58,9 @@ public class DashboardFragment extends MCFragment {
|
|||||||
if (fab != null) {
|
if (fab != null) {
|
||||||
fab.setOnClickListener(v -> showAddToDashboardOptionsDialog());
|
fab.setOnClickListener(v -> showAddToDashboardOptionsDialog());
|
||||||
}
|
}
|
||||||
|
if (mHolder.emptyStateButton != null) {
|
||||||
|
mHolder.emptyStateButton.setOnClickListener(v -> showAddToDashboardOptionsDialog());
|
||||||
|
}
|
||||||
|
|
||||||
setupRecyclerView(mHolder.list);
|
setupRecyclerView(mHolder.list);
|
||||||
loadDashboardMonsters();
|
loadDashboardMonsters();
|
||||||
@@ -66,7 +70,7 @@ public class DashboardFragment extends MCFragment {
|
|||||||
|
|
||||||
private void loadDashboardMonsters() {
|
private void loadDashboardMonsters() {
|
||||||
MonsterRepository repository = getMonsterRepository();
|
MonsterRepository repository = getMonsterRepository();
|
||||||
mDisposables.add(repository.getDashboardMonsters()
|
mDisposables.add(repository.getDashboardMonstersWithMonster()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError));
|
.subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError));
|
||||||
@@ -79,16 +83,32 @@ public class DashboardFragment extends MCFragment {
|
|||||||
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
|
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
|
|
||||||
LiveData<List<Monster>> monsterData = mViewModel.getMonsters();
|
LiveData<List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster>> monsterData = mViewModel.getMonsters();
|
||||||
mAdapter = new DashboardRecyclerViewAdapter(monster -> {
|
mAdapter = new DashboardRecyclerViewAdapter(
|
||||||
if (monster != null) {
|
item -> {
|
||||||
navigateToMonsterDetail(monster);
|
if (item != null && item.monster != null) {
|
||||||
} else {
|
navigateToMonsterDetail(item.monster);
|
||||||
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
|
} else {
|
||||||
}
|
Logger.logError("Can't navigate to MonsterDetailFragment with a null monster");
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
item -> {
|
||||||
|
if (item != null) {
|
||||||
|
showCardOptionsMenu(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
if (monsterData != null) {
|
if (monsterData != null) {
|
||||||
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
|
monsterData.observe(getViewLifecycleOwner(), monsters -> {
|
||||||
|
mAdapter.submitList(monsters);
|
||||||
|
boolean isEmpty = (monsters == null || monsters.isEmpty());
|
||||||
|
if (mHolder.emptyState != null) {
|
||||||
|
mHolder.emptyState.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
if (mHolder.list != null) {
|
||||||
|
mHolder.list.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
recyclerView.setAdapter(mAdapter);
|
recyclerView.setAdapter(mAdapter);
|
||||||
|
|
||||||
@@ -101,9 +121,9 @@ public class DashboardFragment extends MCFragment {
|
|||||||
int fromPos = viewHolder.getAdapterPosition();
|
int fromPos = viewHolder.getAdapterPosition();
|
||||||
int toPos = target.getAdapterPosition();
|
int toPos = target.getAdapterPosition();
|
||||||
if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) {
|
if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) {
|
||||||
List<Monster> currentList = new ArrayList<>(mAdapter.getCurrentList());
|
List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster> currentList = new ArrayList<>(mAdapter.getCurrentList());
|
||||||
if (fromPos < currentList.size() && toPos < currentList.size()) {
|
if (fromPos < currentList.size() && toPos < currentList.size()) {
|
||||||
Monster moved = currentList.remove(fromPos);
|
com.majinnaibu.monstercards.models.DashboardMonsterWithMonster moved = currentList.remove(fromPos);
|
||||||
currentList.add(toPos, moved);
|
currentList.add(toPos, moved);
|
||||||
mAdapter.submitList(currentList);
|
mAdapter.submitList(currentList);
|
||||||
}
|
}
|
||||||
@@ -115,10 +135,10 @@ public class DashboardFragment extends MCFragment {
|
|||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||||
int position = viewHolder.getAdapterPosition();
|
int position = viewHolder.getAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
List<Monster> currentList = mAdapter.getCurrentList();
|
List<com.majinnaibu.monstercards.models.DashboardMonsterWithMonster> currentList = mAdapter.getCurrentList();
|
||||||
if (position < currentList.size()) {
|
if (position < currentList.size()) {
|
||||||
Monster monster = currentList.get(position);
|
com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item = currentList.get(position);
|
||||||
removeMonsterFromDashboard(monster);
|
removeDashboardMonsterEntry(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,23 +150,88 @@ public class DashboardFragment extends MCFragment {
|
|||||||
String[] options = new String[]{
|
String[] options = new String[]{
|
||||||
getString(R.string.action_add_single_monster),
|
getString(R.string.action_add_single_monster),
|
||||||
getString(R.string.action_add_collection_option),
|
getString(R.string.action_add_collection_option),
|
||||||
getString(R.string.action_clear_dashboard)
|
getString(R.string.action_create_monster),
|
||||||
|
getString(R.string.action_import)
|
||||||
};
|
};
|
||||||
new AlertDialog.Builder(requireContext())
|
new AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.dialog_add_to_dashboard)
|
.setTitle(R.string.title_dashboard_actions)
|
||||||
.setItems(options, (dialog, which) -> {
|
.setItems(options, (dialog, which) -> {
|
||||||
if (which == 0) {
|
if (which == 0) {
|
||||||
showAddMonsterPicker();
|
showAddMonsterPicker();
|
||||||
} else if (which == 1) {
|
} else if (which == 1) {
|
||||||
showAddCollectionPicker();
|
showAddCollectionPicker();
|
||||||
} else if (which == 2) {
|
} else if (which == 2) {
|
||||||
clearDashboard();
|
createNewMonster();
|
||||||
|
} else if (which == 3) {
|
||||||
|
showImportDialog();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showRemoveMonsterPicker() {
|
||||||
|
MonsterRepository repository = getMonsterRepository();
|
||||||
|
mDisposables.add(repository.getDashboardMonsters()
|
||||||
|
.firstOrError()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(monsters -> {
|
||||||
|
if (monsters.isEmpty()) {
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_dashboard_already_empty));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] names = new String[monsters.size()];
|
||||||
|
for (int i = 0; i < monsters.size(); i++) {
|
||||||
|
names[i] = monsters.get(i).name;
|
||||||
|
}
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.action_remove_single_monster)
|
||||||
|
.setItems(names, (dialog, which) -> {
|
||||||
|
Monster selected = monsters.get(which);
|
||||||
|
removeMonsterFromDashboard(selected);
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
}, Logger::logError));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCardOptionsMenu(@NonNull com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item) {
|
||||||
|
String[] options = new String[]{
|
||||||
|
getString(R.string.action_view_details),
|
||||||
|
getString(R.string.action_remove_from_dashboard)
|
||||||
|
};
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(item.monster.name)
|
||||||
|
.setItems(options, (dialog, which) -> {
|
||||||
|
if (which == 0) {
|
||||||
|
navigateToMonsterDetail(item.monster);
|
||||||
|
} else if (which == 1) {
|
||||||
|
removeDashboardMonsterEntry(item);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeDashboardMonsterEntry(@NonNull com.majinnaibu.monstercards.models.DashboardMonsterWithMonster item) {
|
||||||
|
MonsterRepository repository = getMonsterRepository();
|
||||||
|
mDisposables.add(repository.removeDashboardMonsterById(item.dashboardEntry.id)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.makeLong(view, getString(R.string.snackbar_removed_from_dashboard, item.monster.name))
|
||||||
|
.setAction(R.string.action_undo, v -> addMonsterToDashboard(item.monster))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}, Logger::logError));
|
||||||
|
}
|
||||||
|
|
||||||
private void showAddMonsterPicker() {
|
private void showAddMonsterPicker() {
|
||||||
MonsterRepository repository = getMonsterRepository();
|
MonsterRepository repository = getMonsterRepository();
|
||||||
mDisposables.add(repository.getMonsters()
|
mDisposables.add(repository.getMonsters()
|
||||||
@@ -157,7 +242,7 @@ public class DashboardFragment extends MCFragment {
|
|||||||
if (monsters.isEmpty()) {
|
if (monsters.isEmpty()) {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.no_monsters_available), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.no_monsters_available));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -178,7 +263,7 @@ public class DashboardFragment extends MCFragment {
|
|||||||
|
|
||||||
private void showAddCollectionPicker() {
|
private void showAddCollectionPicker() {
|
||||||
MonsterRepository repository = getMonsterRepository();
|
MonsterRepository repository = getMonsterRepository();
|
||||||
mDisposables.add(repository.getCollections()
|
mDisposables.add(repository.getCollectionsWithCount()
|
||||||
.firstOrError()
|
.firstOrError()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
@@ -186,18 +271,22 @@ public class DashboardFragment extends MCFragment {
|
|||||||
if (collections.isEmpty()) {
|
if (collections.isEmpty()) {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.no_collections_available), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.no_collections_available));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String[] names = new String[collections.size()];
|
String[] names = new String[collections.size()];
|
||||||
for (int i = 0; i < collections.size(); i++) {
|
for (int i = 0; i < collections.size(); i++) {
|
||||||
names[i] = collections.get(i).name;
|
CollectionWithCount item = collections.get(i);
|
||||||
|
int count = item.monsterCount;
|
||||||
|
String countText = count == 1 ? "1 monster" : count + " monsters";
|
||||||
|
String colName = item.collection != null && item.collection.name != null ? item.collection.name : "";
|
||||||
|
names[i] = colName + " (" + countText + ")";
|
||||||
}
|
}
|
||||||
new AlertDialog.Builder(requireContext())
|
new AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.action_add_collection_option)
|
.setTitle(R.string.action_add_collection_option)
|
||||||
.setItems(names, (dialog, which) -> {
|
.setItems(names, (dialog, which) -> {
|
||||||
Collection selected = collections.get(which);
|
Collection selected = collections.get(which).collection;
|
||||||
addCollectionToDashboard(selected);
|
addCollectionToDashboard(selected);
|
||||||
})
|
})
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
@@ -213,7 +302,7 @@ public class DashboardFragment extends MCFragment {
|
|||||||
.subscribe(() -> {
|
.subscribe(() -> {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monster.name), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_added_to_dashboard, monster.name));
|
||||||
}
|
}
|
||||||
}, Logger::logError));
|
}, Logger::logError));
|
||||||
}
|
}
|
||||||
@@ -226,7 +315,7 @@ public class DashboardFragment extends MCFragment {
|
|||||||
.subscribe(() -> {
|
.subscribe(() -> {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collection.name), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_collection_added_to_dashboard, collection.name));
|
||||||
}
|
}
|
||||||
}, Logger::logError));
|
}, Logger::logError));
|
||||||
}
|
}
|
||||||
@@ -236,7 +325,14 @@ public class DashboardFragment extends MCFragment {
|
|||||||
mDisposables.add(repository.removeMonsterFromDashboard(monster.id)
|
mDisposables.add(repository.removeMonsterFromDashboard(monster.id)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(() -> {}, Logger::logError));
|
.subscribe(() -> {
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.makeLong(view, getString(R.string.snackbar_removed_from_dashboard, monster.name))
|
||||||
|
.setAction(R.string.action_undo, v -> addMonsterToDashboard(monster))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}, Logger::logError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearDashboard() {
|
private void clearDashboard() {
|
||||||
@@ -247,7 +343,7 @@ public class DashboardFragment extends MCFragment {
|
|||||||
.subscribe(() -> {
|
.subscribe(() -> {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.snackbar_dashboard_cleared), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_dashboard_cleared));
|
||||||
}
|
}
|
||||||
}, Logger::logError));
|
}, Logger::logError));
|
||||||
}
|
}
|
||||||
@@ -274,10 +370,25 @@ public class DashboardFragment extends MCFragment {
|
|||||||
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
|
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
|
||||||
clearDashboard();
|
clearDashboard();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_dashboard) {
|
||||||
|
exportDashboard();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void exportDashboard() {
|
||||||
|
mDisposables.add(getMonsterRepository().getDashboardMonsters()
|
||||||
|
.firstOrError()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(monsters -> {
|
||||||
|
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder("", monsters);
|
||||||
|
String fileName = getString(R.string.default_filename_dashboard) + ".binder";
|
||||||
|
exportToFile(fileName, json);
|
||||||
|
}, Logger::logError));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -286,9 +397,13 @@ public class DashboardFragment extends MCFragment {
|
|||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
final RecyclerView list;
|
final RecyclerView list;
|
||||||
|
final View emptyState;
|
||||||
|
final View emptyStateButton;
|
||||||
|
|
||||||
ViewHolder(View root) {
|
ViewHolder(View root) {
|
||||||
list = root.findViewById(R.id.list);
|
list = root.findViewById(R.id.list);
|
||||||
|
emptyState = root.findViewById(R.id.empty_state);
|
||||||
|
emptyStateButton = root.findViewById(R.id.empty_state_button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,29 +18,44 @@ import com.majinnaibu.monstercards.data.enums.ChallengeRating;
|
|||||||
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
import com.majinnaibu.monstercards.data.enums.ProficiencyType;
|
||||||
import com.majinnaibu.monstercards.databinding.CardMonsterBinding;
|
import com.majinnaibu.monstercards.databinding.CardMonsterBinding;
|
||||||
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
|
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
|
||||||
|
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.models.Trait;
|
import com.majinnaibu.monstercards.models.Trait;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, DashboardRecyclerViewAdapter.ViewHolder> {
|
public class DashboardRecyclerViewAdapter extends ListAdapter<DashboardMonsterWithMonster, DashboardRecyclerViewAdapter.ViewHolder> {
|
||||||
private static final DiffUtil.ItemCallback<Monster> DIFF_CALLBACK = new DiffUtil.ItemCallback<Monster>() {
|
private static final DiffUtil.ItemCallback<DashboardMonsterWithMonster> DIFF_CALLBACK = new DiffUtil.ItemCallback<DashboardMonsterWithMonster>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean areItemsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
|
public boolean areItemsTheSame(@NonNull DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) {
|
||||||
return oldItem.id.equals(newItem.id);
|
if (oldItem.dashboardEntry != null && newItem.dashboardEntry != null) {
|
||||||
|
return oldItem.dashboardEntry.id == newItem.dashboardEntry.id;
|
||||||
|
}
|
||||||
|
return oldItem.monster.id.equals(newItem.monster.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean areContentsTheSame(@NonNull Monster oldItem, @NonNull Monster newItem) {
|
public boolean areContentsTheSame(@NonNull DashboardMonsterWithMonster oldItem, @NonNull DashboardMonsterWithMonster newItem) {
|
||||||
return oldItem.equals(newItem);
|
if (oldItem.dashboardEntry != null && newItem.dashboardEntry != null) {
|
||||||
|
return oldItem.dashboardEntry.id == newItem.dashboardEntry.id
|
||||||
|
&& oldItem.dashboardEntry.ordinal == newItem.dashboardEntry.ordinal
|
||||||
|
&& (oldItem.monster != null && oldItem.monster.equals(newItem.monster));
|
||||||
|
}
|
||||||
|
return (oldItem.monster != null && oldItem.monster.equals(newItem.monster));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private final ItemCallback mOnClick;
|
private final ItemCallback mOnClick;
|
||||||
|
private final ItemCallback mOnLongClick;
|
||||||
|
|
||||||
public DashboardRecyclerViewAdapter(ItemCallback onClick) {
|
public DashboardRecyclerViewAdapter(ItemCallback onClick, ItemCallback onLongClick) {
|
||||||
super(DIFF_CALLBACK);
|
super(DIFF_CALLBACK);
|
||||||
mOnClick = onClick;
|
mOnClick = onClick;
|
||||||
|
mOnLongClick = onLongClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DashboardRecyclerViewAdapter(ItemCallback onClick) {
|
||||||
|
this(onClick, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -52,7 +67,9 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, Dashboard
|
|||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
Logger.logUnimplementedMethod();
|
Logger.logUnimplementedMethod();
|
||||||
Monster monster = getItem(position);
|
DashboardMonsterWithMonster item = getItem(position);
|
||||||
|
Monster monster = item.monster;
|
||||||
|
holder.item = item;
|
||||||
holder.monster = monster;
|
holder.monster = monster;
|
||||||
holder.name.setText(monster.name);
|
holder.name.setText(monster.name);
|
||||||
holder.meta.setText(monster.getMeta());
|
holder.meta.setText(monster.getMeta());
|
||||||
@@ -120,16 +137,25 @@ public class DashboardRecyclerViewAdapter extends ListAdapter<Monster, Dashboard
|
|||||||
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
holder.itemView.setOnClickListener(v -> {
|
||||||
if (mOnClick != null) {
|
if (mOnClick != null) {
|
||||||
mOnClick.onItemCallback(holder.monster);
|
mOnClick.onItemCallback(holder.item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener(v -> {
|
||||||
|
if (mOnLongClick != null) {
|
||||||
|
mOnLongClick.onItemCallback(holder.item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ItemCallback {
|
public interface ItemCallback {
|
||||||
void onItemCallback(Monster monster);
|
void onItemCallback(DashboardMonsterWithMonster item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
public DashboardMonsterWithMonster item;
|
||||||
public final TextView name;
|
public final TextView name;
|
||||||
public final TextView meta;
|
public final TextView meta;
|
||||||
public final View action1Group;
|
public final View action1Group;
|
||||||
|
|||||||
@@ -4,23 +4,23 @@ import androidx.lifecycle.LiveData;
|
|||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.DashboardMonsterWithMonster;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class DashboardViewModel extends ViewModel {
|
public class DashboardViewModel extends ViewModel {
|
||||||
private final MutableLiveData<List<Monster>> mMonsters;
|
private final MutableLiveData<List<DashboardMonsterWithMonster>> mMonsters;
|
||||||
|
|
||||||
public DashboardViewModel() {
|
public DashboardViewModel() {
|
||||||
mMonsters = new MutableLiveData<>(new ArrayList<>());
|
mMonsters = new MutableLiveData<>(new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<Monster>> getMonsters() {
|
public LiveData<List<DashboardMonsterWithMonster>> getMonsters() {
|
||||||
return mMonsters;
|
return mMonsters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMonsters(List<Monster> monsters) {
|
public void setMonsters(List<DashboardMonsterWithMonster> monsters) {
|
||||||
mMonsters.setValue(monsters);
|
mMonsters.setValue(monsters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.majinnaibu.monstercards.ui.editmonster;
|
package com.majinnaibu.monstercards.ui.editmonster;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.activity.OnBackPressedCallback;
|
import androidx.activity.OnBackPressedCallback;
|
||||||
@@ -14,18 +16,19 @@ import androidx.lifecycle.ViewModelProvider;
|
|||||||
import androidx.navigation.NavBackStackEntry;
|
import androidx.navigation.NavBackStackEntry;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.data.enums.StringType;
|
import com.majinnaibu.monstercards.data.enums.StringType;
|
||||||
import com.majinnaibu.monstercards.data.enums.TraitType;
|
import com.majinnaibu.monstercards.data.enums.TraitType;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentArgs;
|
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -43,21 +46,34 @@ public class EditMonsterFragment extends MCFragment {
|
|||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@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);
|
View root = inflater.inflate(R.layout.fragment_edit_monster, container, false);
|
||||||
mHolder = new ViewHolder(root);
|
mHolder = new ViewHolder(root);
|
||||||
|
|
||||||
|
MonsterRepository repository = getMonsterRepository();
|
||||||
|
Bundle arguments = getArguments();
|
||||||
|
if (arguments == null) {
|
||||||
|
Logger.logError("Arguments bundle is null in EditMonsterFragment");
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
String monsterIdStr = EditMonsterFragmentArgs.fromBundle(arguments).getMonsterId();
|
||||||
|
if (monsterIdStr == null || monsterIdStr.isEmpty()) {
|
||||||
|
Logger.logError("monsterId is null or empty in EditMonsterFragment");
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID monsterId = UUID.fromString(monsterIdStr);
|
||||||
|
|
||||||
|
try {
|
||||||
|
NavController navController = NavHostFragment.findNavController(this);
|
||||||
|
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation);
|
||||||
|
mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
mViewModel = new ViewModelProvider(this).get(EditMonsterViewModel.class);
|
||||||
|
}
|
||||||
|
|
||||||
setTitle(getString(R.string.title_editMonster_fmt, getString(R.string.default_monster_name)));
|
setTitle(getString(R.string.title_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)) {
|
if (mViewModel.hasError() || !mViewModel.hasLoaded() || !Objects.equals(mViewModel.getMonsterId().getValue(), monsterId)) {
|
||||||
repository.getMonster(monsterId).toObservable()
|
repository.getMonster(monsterId).toObservable()
|
||||||
.firstOrError()
|
.firstOrError()
|
||||||
@@ -73,7 +89,6 @@ public class EditMonsterFragment extends MCFragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
||||||
// TODO: Show an error state.
|
|
||||||
Logger.logError(e);
|
Logger.logError(e);
|
||||||
mViewModel.setHasError(true);
|
mViewModel.setHasError(true);
|
||||||
mViewModel.setErrorMessage(e.toString());
|
mViewModel.setErrorMessage(e.toString());
|
||||||
@@ -82,101 +97,9 @@ public class EditMonsterFragment extends MCFragment {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mHolder.basicInfoButton.setOnClickListener(v -> {
|
setupAccordions();
|
||||||
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment();
|
setupSummaries();
|
||||||
Navigation.findNavController(requireView()).navigate(action);
|
setupSubClickListeners();
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||||
@Override
|
@Override
|
||||||
@@ -187,7 +110,6 @@ public class EditMonsterFragment extends MCFragment {
|
|||||||
alertDialog.setTitle("Unsaved Changes");
|
alertDialog.setTitle("Unsaved Changes");
|
||||||
alertDialog.setMessage("Do you want to save your changes?");
|
alertDialog.setMessage("Do you want to save your changes?");
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Save", (dialog, id) -> {
|
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())
|
getMonsterRepository().saveMonster(mViewModel.buildMonster())
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
@@ -195,29 +117,22 @@ public class EditMonsterFragment extends MCFragment {
|
|||||||
new DisposableCompletableObserver() {
|
new DisposableCompletableObserver() {
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() {
|
||||||
Navigation.findNavController(requireView()).navigateUp();
|
NavHostFragment.findNavController(EditMonsterFragment.this).navigateUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
||||||
Logger.logError("Error creating monster", e);
|
Logger.logError("Error saving monster", e);
|
||||||
assert view != null;
|
assert view != null;
|
||||||
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG)
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster));
|
||||||
.setAction("Action", null).show();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Discard", (dialog, id) -> {
|
alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Discard", (dialog, id) -> NavHostFragment.findNavController(EditMonsterFragment.this).navigateUp());
|
||||||
// Navigate up ignoring unsaved changes.
|
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "Cancel", (dialog, id) -> {});
|
||||||
Navigation.findNavController(requireView()).navigateUp();
|
|
||||||
});
|
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "Cancel", (dialog, id) -> {
|
|
||||||
// Do nothing.
|
|
||||||
});
|
|
||||||
alertDialog.show();
|
alertDialog.show();
|
||||||
} else {
|
} else {
|
||||||
// No changes so we can safely leave.
|
NavHostFragment.findNavController(EditMonsterFragment.this).navigateUp();
|
||||||
Navigation.findNavController(requireView()).navigateUp();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -225,42 +140,344 @@ public class EditMonsterFragment extends MCFragment {
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ViewHolder {
|
private void setupAccordions() {
|
||||||
|
if (mHolder.headerBasicInfo != null && mHolder.bodyBasicInfo != null && mHolder.chevronBasicInfo != null) {
|
||||||
|
setupAccordion(mHolder.headerBasicInfo, mHolder.bodyBasicInfo, mHolder.chevronBasicInfo);
|
||||||
|
}
|
||||||
|
if (mHolder.headerCombatAbilities != null && mHolder.bodyCombatAbilities != null && mHolder.chevronCombatAbilities != null) {
|
||||||
|
setupAccordion(mHolder.headerCombatAbilities, mHolder.bodyCombatAbilities, mHolder.chevronCombatAbilities);
|
||||||
|
}
|
||||||
|
if (mHolder.headerSavesSkillsLanguages != null && mHolder.bodySavesSkillsLanguages != null && mHolder.chevronSavesSkillsLanguages != null) {
|
||||||
|
setupAccordion(mHolder.headerSavesSkillsLanguages, mHolder.bodySavesSkillsLanguages, mHolder.chevronSavesSkillsLanguages);
|
||||||
|
}
|
||||||
|
if (mHolder.headerDefenses != null && mHolder.bodyDefenses != null && mHolder.chevronDefenses != null) {
|
||||||
|
setupAccordion(mHolder.headerDefenses, mHolder.bodyDefenses, mHolder.chevronDefenses);
|
||||||
|
}
|
||||||
|
if (mHolder.headerTraitsActions != null && mHolder.bodyTraitsActions != null && mHolder.chevronTraitsActions != null) {
|
||||||
|
setupAccordion(mHolder.headerTraitsActions, mHolder.bodyTraitsActions, mHolder.chevronTraitsActions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextView basicInfoButton;
|
private void setupAccordion(View header, View body, ImageView chevron) {
|
||||||
TextView armorButton;
|
header.setOnClickListener(v -> {
|
||||||
TextView speedButton;
|
boolean isExpanded = body.getVisibility() == View.VISIBLE;
|
||||||
TextView abilityScoresButton;
|
body.setVisibility(isExpanded ? View.GONE : View.VISIBLE);
|
||||||
TextView savingThrows;
|
chevron.setImageResource(isExpanded ? R.drawable.ic_expand_more_24 : R.drawable.ic_expand_less_24);
|
||||||
TextView skills;
|
});
|
||||||
TextView conditionImmunities;
|
}
|
||||||
TextView damageImmunities;
|
|
||||||
TextView damageResistances;
|
private void setupSummaries() {
|
||||||
TextView damageVulnerabilities;
|
mViewModel.getName().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
|
||||||
TextView senses;
|
mViewModel.getSize().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
|
||||||
TextView languages;
|
mViewModel.getType().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
|
||||||
TextView challengeRating;
|
mViewModel.getChallengeRating().observe(getViewLifecycleOwner(), v -> updateBasicInfoSummary());
|
||||||
TextView abilities;
|
|
||||||
TextView actions;
|
mViewModel.getStrength().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
|
||||||
TextView reactions;
|
mViewModel.getDexterity().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
|
||||||
TextView legendaryActions;
|
mViewModel.getConstitution().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
|
||||||
TextView lairActions;
|
mViewModel.getIntelligence().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
|
||||||
TextView regionalActions;
|
mViewModel.getWisdom().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
|
||||||
|
mViewModel.getCharisma().observe(getViewLifecycleOwner(), v -> updateCombatAbilitiesSummary());
|
||||||
|
|
||||||
|
mViewModel.getSkills().observe(getViewLifecycleOwner(), v -> updateSavesSkillsSummary());
|
||||||
|
mViewModel.getLanguages().observe(getViewLifecycleOwner(), v -> updateSavesSkillsSummary());
|
||||||
|
|
||||||
|
mViewModel.getConditionImmunities().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
|
||||||
|
mViewModel.getDamageImmunities().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
|
||||||
|
mViewModel.getDamageResistances().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
|
||||||
|
mViewModel.getDamageVulnerabilities().observe(getViewLifecycleOwner(), v -> updateDefensesSummary());
|
||||||
|
|
||||||
|
mViewModel.getAbilities().observe(getViewLifecycleOwner(), v -> updateTraitsActionsSummary());
|
||||||
|
mViewModel.getActions().observe(getViewLifecycleOwner(), v -> updateTraitsActionsSummary());
|
||||||
|
mViewModel.getReactions().observe(getViewLifecycleOwner(), v -> updateTraitsActionsSummary());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBasicInfoSummary() {
|
||||||
|
if (mHolder.summaryBasicInfo == null) return;
|
||||||
|
String name = mViewModel.getName().getValue();
|
||||||
|
String size = mViewModel.getSize().getValue();
|
||||||
|
String type = mViewModel.getType().getValue();
|
||||||
|
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
if (!TextUtils.isEmpty(name)) parts.add(name);
|
||||||
|
if (!TextUtils.isEmpty(size)) parts.add(size);
|
||||||
|
if (!TextUtils.isEmpty(type)) parts.add(type);
|
||||||
|
|
||||||
|
mHolder.summaryBasicInfo.setText(parts.isEmpty() ? "Tap to edit basic info" : TextUtils.join(", ", parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCombatAbilitiesSummary() {
|
||||||
|
if (mHolder.summaryCombatAbilities == null) return;
|
||||||
|
Integer str = mViewModel.getStrength().getValue();
|
||||||
|
Integer dex = mViewModel.getDexterity().getValue();
|
||||||
|
Integer con = mViewModel.getConstitution().getValue();
|
||||||
|
Integer intel = mViewModel.getIntelligence().getValue();
|
||||||
|
Integer wis = mViewModel.getWisdom().getValue();
|
||||||
|
Integer cha = mViewModel.getCharisma().getValue();
|
||||||
|
|
||||||
|
String summary = String.format("STR %d | DEX %d | CON %d | INT %d | WIS %d | CHA %d",
|
||||||
|
str != null ? str : 10,
|
||||||
|
dex != null ? dex : 10,
|
||||||
|
con != null ? con : 10,
|
||||||
|
intel != null ? intel : 10,
|
||||||
|
wis != null ? wis : 10,
|
||||||
|
cha != null ? cha : 10);
|
||||||
|
mHolder.summaryCombatAbilities.setText(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSavesSkillsSummary() {
|
||||||
|
if (mHolder.summarySavesSkillsLanguages == null) return;
|
||||||
|
int skillsCount = mViewModel.getSkills().getValue() != null ? mViewModel.getSkills().getValue().size() : 0;
|
||||||
|
int languagesCount = mViewModel.getLanguages().getValue() != null ? mViewModel.getLanguages().getValue().size() : 0;
|
||||||
|
|
||||||
|
mHolder.summarySavesSkillsLanguages.setText(String.format("%d Skills, %d Languages", skillsCount, languagesCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDefensesSummary() {
|
||||||
|
if (mHolder.summaryDefenses == null) return;
|
||||||
|
List<String> defenses = new ArrayList<>();
|
||||||
|
if (mViewModel.getConditionImmunities().getValue() != null && !mViewModel.getConditionImmunities().getValue().isEmpty()) {
|
||||||
|
defenses.add(mViewModel.getConditionImmunities().getValue().size() + " Condition Immunities");
|
||||||
|
}
|
||||||
|
if (mViewModel.getDamageImmunities().getValue() != null && !mViewModel.getDamageImmunities().getValue().isEmpty()) {
|
||||||
|
defenses.add(mViewModel.getDamageImmunities().getValue().size() + " Damage Immunities");
|
||||||
|
}
|
||||||
|
if (mViewModel.getDamageResistances().getValue() != null && !mViewModel.getDamageResistances().getValue().isEmpty()) {
|
||||||
|
defenses.add(mViewModel.getDamageResistances().getValue().size() + " Resistances");
|
||||||
|
}
|
||||||
|
|
||||||
|
mHolder.summaryDefenses.setText(defenses.isEmpty() ? "None" : TextUtils.join(", ", defenses));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTraitsActionsSummary() {
|
||||||
|
if (mHolder.summaryTraitsActions == null) return;
|
||||||
|
int abilitiesCount = mViewModel.getAbilities().getValue() != null ? mViewModel.getAbilities().getValue().size() : 0;
|
||||||
|
int actionsCount = mViewModel.getActions().getValue() != null ? mViewModel.getActions().getValue().size() : 0;
|
||||||
|
int reactionsCount = mViewModel.getReactions().getValue() != null ? mViewModel.getReactions().getValue().size() : 0;
|
||||||
|
|
||||||
|
mHolder.summaryTraitsActions.setText(String.format("%d Abilities, %d Actions, %d Reactions", abilitiesCount, actionsCount, reactionsCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupSubClickListeners() {
|
||||||
|
if (mHolder.basicInfoButton != null) {
|
||||||
|
mHolder.basicInfoButton.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditBasicInfoFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.challengeRating != null) {
|
||||||
|
mHolder.challengeRating.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditChallengeRatingFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.armorButton != null) {
|
||||||
|
mHolder.armorButton.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditArmorFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.speedButton != null) {
|
||||||
|
mHolder.speedButton.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSpeedFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.abilityScoresButton != null) {
|
||||||
|
mHolder.abilityScoresButton.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditAbilityScoresFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.savingThrows != null) {
|
||||||
|
mHolder.savingThrows.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSavingThrowsFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.skills != null) {
|
||||||
|
mHolder.skills.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditSkillsFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.senses != null) {
|
||||||
|
mHolder.senses.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.SENSE);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.languages != null) {
|
||||||
|
mHolder.languages.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditLanguagesFragment();
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.conditionImmunities != null) {
|
||||||
|
mHolder.conditionImmunities.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.CONDITION_IMMUNITY);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.damageImmunities != null) {
|
||||||
|
mHolder.damageImmunities.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_IMMUNITY);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.damageResistances != null) {
|
||||||
|
mHolder.damageResistances.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_RESISTANCE);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.damageVulnerabilities != null) {
|
||||||
|
mHolder.damageVulnerabilities.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditStringsFragment(StringType.DAMAGE_VULNERABILITY);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.abilities != null) {
|
||||||
|
mHolder.abilities.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ABILITY);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.actions != null) {
|
||||||
|
mHolder.actions.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.ACTION);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.reactions != null) {
|
||||||
|
mHolder.reactions.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REACTIONS);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.legendaryActions != null) {
|
||||||
|
mHolder.legendaryActions.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LEGENDARY_ACTION);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.lairActions != null) {
|
||||||
|
mHolder.lairActions.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.LAIR_ACTION);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHolder.regionalActions != null) {
|
||||||
|
mHolder.regionalActions.setOnClickListener(v -> {
|
||||||
|
NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditTraitListFragment(TraitType.REGIONAL_ACTION);
|
||||||
|
NavHostFragment.findNavController(this).navigate(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ViewHolder {
|
||||||
|
final View headerBasicInfo;
|
||||||
|
final View bodyBasicInfo;
|
||||||
|
final ImageView chevronBasicInfo;
|
||||||
|
final TextView summaryBasicInfo;
|
||||||
|
|
||||||
|
final View headerCombatAbilities;
|
||||||
|
final View bodyCombatAbilities;
|
||||||
|
final ImageView chevronCombatAbilities;
|
||||||
|
final TextView summaryCombatAbilities;
|
||||||
|
|
||||||
|
final View headerSavesSkillsLanguages;
|
||||||
|
final View bodySavesSkillsLanguages;
|
||||||
|
final ImageView chevronSavesSkillsLanguages;
|
||||||
|
final TextView summarySavesSkillsLanguages;
|
||||||
|
|
||||||
|
final View headerDefenses;
|
||||||
|
final View bodyDefenses;
|
||||||
|
final ImageView chevronDefenses;
|
||||||
|
final TextView summaryDefenses;
|
||||||
|
|
||||||
|
final View headerTraitsActions;
|
||||||
|
final View bodyTraitsActions;
|
||||||
|
final ImageView chevronTraitsActions;
|
||||||
|
final TextView summaryTraitsActions;
|
||||||
|
|
||||||
|
final TextView basicInfoButton;
|
||||||
|
final TextView challengeRating;
|
||||||
|
final TextView armorButton;
|
||||||
|
final TextView speedButton;
|
||||||
|
final TextView abilityScoresButton;
|
||||||
|
final TextView savingThrows;
|
||||||
|
final TextView skills;
|
||||||
|
final TextView senses;
|
||||||
|
final TextView languages;
|
||||||
|
final TextView conditionImmunities;
|
||||||
|
final TextView damageImmunities;
|
||||||
|
final TextView damageResistances;
|
||||||
|
final TextView damageVulnerabilities;
|
||||||
|
final TextView abilities;
|
||||||
|
final TextView actions;
|
||||||
|
final TextView reactions;
|
||||||
|
final TextView legendaryActions;
|
||||||
|
final TextView lairActions;
|
||||||
|
final TextView regionalActions;
|
||||||
|
|
||||||
ViewHolder(@NonNull View root) {
|
ViewHolder(@NonNull View root) {
|
||||||
|
headerBasicInfo = root.findViewById(R.id.headerBasicInfo);
|
||||||
|
bodyBasicInfo = root.findViewById(R.id.bodyBasicInfo);
|
||||||
|
chevronBasicInfo = root.findViewById(R.id.chevronBasicInfo);
|
||||||
|
summaryBasicInfo = root.findViewById(R.id.summaryBasicInfo);
|
||||||
|
|
||||||
|
headerCombatAbilities = root.findViewById(R.id.headerCombatAbilities);
|
||||||
|
bodyCombatAbilities = root.findViewById(R.id.bodyCombatAbilities);
|
||||||
|
chevronCombatAbilities = root.findViewById(R.id.chevronCombatAbilities);
|
||||||
|
summaryCombatAbilities = root.findViewById(R.id.summaryCombatAbilities);
|
||||||
|
|
||||||
|
headerSavesSkillsLanguages = root.findViewById(R.id.headerSavesSkillsLanguages);
|
||||||
|
bodySavesSkillsLanguages = root.findViewById(R.id.bodySavesSkillsLanguages);
|
||||||
|
chevronSavesSkillsLanguages = root.findViewById(R.id.chevronSavesSkillsLanguages);
|
||||||
|
summarySavesSkillsLanguages = root.findViewById(R.id.summarySavesSkillsLanguages);
|
||||||
|
|
||||||
|
headerDefenses = root.findViewById(R.id.headerDefenses);
|
||||||
|
bodyDefenses = root.findViewById(R.id.bodyDefenses);
|
||||||
|
chevronDefenses = root.findViewById(R.id.chevronDefenses);
|
||||||
|
summaryDefenses = root.findViewById(R.id.summaryDefenses);
|
||||||
|
|
||||||
|
headerTraitsActions = root.findViewById(R.id.headerTraitsActions);
|
||||||
|
bodyTraitsActions = root.findViewById(R.id.bodyTraitsActions);
|
||||||
|
chevronTraitsActions = root.findViewById(R.id.chevronTraitsActions);
|
||||||
|
summaryTraitsActions = root.findViewById(R.id.summaryTraitsActions);
|
||||||
|
|
||||||
basicInfoButton = root.findViewById(R.id.basicInfo);
|
basicInfoButton = root.findViewById(R.id.basicInfo);
|
||||||
|
challengeRating = root.findViewById(R.id.challengeRating);
|
||||||
armorButton = root.findViewById(R.id.armor);
|
armorButton = root.findViewById(R.id.armor);
|
||||||
speedButton = root.findViewById(R.id.speed);
|
speedButton = root.findViewById(R.id.speed);
|
||||||
abilityScoresButton = root.findViewById(R.id.abilityScores);
|
abilityScoresButton = root.findViewById(R.id.abilityScores);
|
||||||
savingThrows = root.findViewById(R.id.savingThrows);
|
savingThrows = root.findViewById(R.id.savingThrows);
|
||||||
skills = root.findViewById(R.id.skills);
|
skills = root.findViewById(R.id.skills);
|
||||||
|
senses = root.findViewById(R.id.senses);
|
||||||
|
languages = root.findViewById(R.id.languages);
|
||||||
conditionImmunities = root.findViewById(R.id.conditionImmunities);
|
conditionImmunities = root.findViewById(R.id.conditionImmunities);
|
||||||
damageImmunities = root.findViewById(R.id.damageImmunities);
|
damageImmunities = root.findViewById(R.id.damageImmunities);
|
||||||
damageResistances = root.findViewById(R.id.damageResistances);
|
damageResistances = root.findViewById(R.id.damageResistances);
|
||||||
damageVulnerabilities = root.findViewById(R.id.damageVulnerabilities);
|
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);
|
abilities = root.findViewById(R.id.abilities);
|
||||||
actions = root.findViewById(R.id.actions);
|
actions = root.findViewById(R.id.actions);
|
||||||
reactions = root.findViewById(R.id.reactions);
|
reactions = root.findViewById(R.id.reactions);
|
||||||
|
|||||||
@@ -702,6 +702,38 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel {
|
|||||||
return mDamageResistances;
|
return mDamageResistances;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<List<String>> getDamageVulnerabilities() {
|
||||||
|
return mDamageVulnerabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<String>> getConditionImmunities() {
|
||||||
|
return mConditionImmunities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Trait>> getAbilities() {
|
||||||
|
return mAbilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Trait>> getActions() {
|
||||||
|
return mActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Trait>> getReactions() {
|
||||||
|
return mReactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Trait>> getLairActions() {
|
||||||
|
return mLairActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Trait>> getLegendaryActions() {
|
||||||
|
return mLegendaryActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Trait>> getRegionalActions() {
|
||||||
|
return mRegionalActions;
|
||||||
|
}
|
||||||
|
|
||||||
public LiveData<List<Language>> getLanguages() {
|
public LiveData<List<Language>> getLanguages() {
|
||||||
return mLanguages;
|
return mLanguages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
@@ -23,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.MainActivity;
|
import com.majinnaibu.monstercards.MainActivity;
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
@@ -34,14 +34,17 @@ import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentDirections;
|
|||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
|
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
|
import io.reactivex.rxjava3.observers.DisposableCompletableObserver;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class LibraryFragment extends MCFragment {
|
public class LibraryFragment extends MCFragment {
|
||||||
|
private final CompositeDisposable mDisposables = new CompositeDisposable();
|
||||||
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
ViewGroup container, Bundle savedInstanceState) {
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
@@ -52,9 +55,15 @@ public class LibraryFragment extends MCFragment {
|
|||||||
assert fab != null;
|
assert fab != null;
|
||||||
setupAddMonsterButton(fab);
|
setupAddMonsterButton(fab);
|
||||||
|
|
||||||
|
View emptyState = root.findViewById(R.id.empty_state);
|
||||||
|
View emptyStateButton = root.findViewById(R.id.empty_state_button);
|
||||||
|
if (emptyStateButton != null) {
|
||||||
|
emptyStateButton.setOnClickListener(v -> createNewMonster());
|
||||||
|
}
|
||||||
|
|
||||||
final RecyclerView recyclerView = root.findViewById(R.id.monster_list);
|
final RecyclerView recyclerView = root.findViewById(R.id.monster_list);
|
||||||
assert recyclerView != null;
|
assert recyclerView != null;
|
||||||
setupRecyclerView(recyclerView);
|
setupRecyclerView(recyclerView, emptyState);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
@@ -67,58 +76,78 @@ public class LibraryFragment extends MCFragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
if (item.getItemId() == R.id.menu_action_import_from_url) {
|
if (item.getItemId() == R.id.menu_action_import) {
|
||||||
showImportUrlDialog();
|
showImportDialog();
|
||||||
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_library) {
|
||||||
|
exportLibrary();
|
||||||
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_everything) {
|
||||||
|
exportEverything();
|
||||||
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_clear_all_data) {
|
||||||
|
showClearAllDataConfirmationDialog();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showImportUrlDialog() {
|
private void showClearAllDataConfirmationDialog() {
|
||||||
Context context = requireContext();
|
new AlertDialog.Builder(requireContext())
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
.setTitle(R.string.dialog_clear_all_data_title)
|
||||||
builder.setTitle(R.string.dialog_import_url_title);
|
.setMessage(R.string.dialog_clear_all_data_message)
|
||||||
builder.setMessage(R.string.dialog_import_url_message);
|
.setPositiveButton(R.string.action_clear, (dialog, which) -> {
|
||||||
|
mDisposables.add(getMonsterRepository().clearAllData()
|
||||||
final EditText input = new EditText(context);
|
.subscribeOn(Schedulers.io())
|
||||||
input.setHint(R.string.dialog_import_url_hint);
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
input.setSingleLine(true);
|
.subscribe(() -> {
|
||||||
|
View view = getView();
|
||||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
if (view != null) {
|
||||||
if (clipboard != null && clipboard.hasPrimaryClip()) {
|
SnackbarHelper.showLong(view, R.string.snackbar_all_data_cleared);
|
||||||
ClipData clip = clipboard.getPrimaryClip();
|
}
|
||||||
if (clip != null && clip.getItemCount() > 0) {
|
}, throwable -> Logger.logError("Failed to clear all data", throwable)));
|
||||||
CharSequence clipText = clip.getItemAt(0).getText();
|
})
|
||||||
if (clipText != null) {
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
String clipStr = clipText.toString().trim();
|
.show();
|
||||||
if (new DnDBeyondImporter().canImport(clipStr) || new Open5eImporter().canImport(clipStr)) {
|
|
||||||
input.setText(clipStr);
|
|
||||||
input.selectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setView(input);
|
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.dialog_import, (dialog, which) -> {
|
|
||||||
String urlOrId = input.getText().toString().trim();
|
|
||||||
if (!urlOrId.isEmpty()) {
|
|
||||||
if (getActivity() instanceof MainActivity) {
|
|
||||||
((MainActivity) getActivity()).importMonsterFromInputAndNavigate(urlOrId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.setNegativeButton(R.string.dialog_cancel, (dialog, which) -> dialog.cancel());
|
|
||||||
|
|
||||||
builder.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
|
private void exportLibrary() {
|
||||||
|
getMonsterRepository().getMonsters()
|
||||||
|
.firstOrError()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(monsters -> {
|
||||||
|
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder("", monsters);
|
||||||
|
String fileName = getString(R.string.default_filename_library) + ".binder";
|
||||||
|
exportToFile(fileName, json);
|
||||||
|
}, Logger::logError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportEverything() {
|
||||||
|
mDisposables.add(getMonsterRepository().exportEverything()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(json -> {
|
||||||
|
String fileName = getString(R.string.default_filename_export_everything) + ".binder";
|
||||||
|
exportToFile(fileName, json);
|
||||||
|
}, throwable -> Logger.logError("Failed to export everything", throwable)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) {
|
||||||
Context context = requireContext();
|
Context context = requireContext();
|
||||||
MonsterRepository repository = this.getMonsterRepository();
|
MonsterRepository repository = this.getMonsterRepository();
|
||||||
|
|
||||||
|
mDisposables.add(repository.getMonsters()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(monsters -> {
|
||||||
|
boolean isEmpty = (monsters == null || monsters.isEmpty());
|
||||||
|
if (emptyState != null) {
|
||||||
|
emptyState.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
recyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
|
||||||
|
}, Logger::logError));
|
||||||
|
|
||||||
LibraryRecyclerViewAdapter adapter = new LibraryRecyclerViewAdapter(
|
LibraryRecyclerViewAdapter adapter = new LibraryRecyclerViewAdapter(
|
||||||
context,
|
context,
|
||||||
repository.getMonsters(),
|
repository.getMonsters(),
|
||||||
@@ -150,30 +179,25 @@ public class LibraryFragment extends MCFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupAddMonsterButton(@NonNull FloatingActionButton fab) {
|
private void setupAddMonsterButton(@NonNull FloatingActionButton fab) {
|
||||||
fab.setOnClickListener(view -> {
|
fab.setOnClickListener(view -> showLibraryFabOptionsDialog());
|
||||||
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() {
|
|
||||||
navigateToEditMonster(monster.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
private void showLibraryFabOptionsDialog() {
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
String[] options = new String[]{
|
||||||
Logger.logError("Error creating monster", e);
|
getString(R.string.action_create_monster),
|
||||||
View view = getView();
|
getString(R.string.action_import)
|
||||||
if (view != null) {
|
};
|
||||||
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
|
new AlertDialog.Builder(requireContext())
|
||||||
}
|
.setTitle(R.string.title_library_actions)
|
||||||
}
|
.setItems(options, (dialog, which) -> {
|
||||||
});
|
if (which == 0) {
|
||||||
});
|
createNewMonster();
|
||||||
|
} else if (which == 1) {
|
||||||
|
showImportDialog();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void navigateToMonsterDetail(@NonNull UUID monsterId) {
|
protected void navigateToMonsterDetail(@NonNull UUID monsterId) {
|
||||||
@@ -188,4 +212,10 @@ public class LibraryFragment extends MCFragment {
|
|||||||
action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
|
action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString());
|
||||||
navController.navigate(action);
|
navController.navigate(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
mDisposables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import androidx.lifecycle.ViewModelProvider;
|
|||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
|
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
|
||||||
@@ -40,6 +39,7 @@ import com.majinnaibu.monstercards.models.Collection;
|
|||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -195,10 +195,25 @@ public class MonsterDetailFragment extends MCFragment {
|
|||||||
} else if (item.getItemId() == R.id.menu_action_share_monster) {
|
} else if (item.getItemId() == R.id.menu_action_share_monster) {
|
||||||
shareCurrentMonster();
|
shareCurrentMonster();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_card) {
|
||||||
|
exportCurrentMonsterCard();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void exportCurrentMonsterCard() {
|
||||||
|
Monster monster = mViewModel.getMonster();
|
||||||
|
if (monster == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String jsonCard = new com.majinnaibu.monstercards.exporters.MonsterCardExporter().exportCard(monster);
|
||||||
|
String safeName = (monster.name != null && !monster.name.trim().isEmpty())
|
||||||
|
? monster.name.trim()
|
||||||
|
: "monster";
|
||||||
|
exportToFile(safeName + ".card", jsonCard);
|
||||||
|
}
|
||||||
|
|
||||||
private void shareCurrentMonster() {
|
private void shareCurrentMonster() {
|
||||||
Monster monster = mViewModel.getMonster();
|
Monster monster = mViewModel.getMonster();
|
||||||
if (monster == null) {
|
if (monster == null) {
|
||||||
@@ -237,7 +252,7 @@ public class MonsterDetailFragment extends MCFragment {
|
|||||||
Logger.logError("Failed to share monster", e);
|
Logger.logError("Failed to share monster", e);
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, R.string.failed_to_share_monster, Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, R.string.failed_to_share_monster);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +270,7 @@ public class MonsterDetailFragment extends MCFragment {
|
|||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
String monsterName = mViewModel.getName().getValue();
|
String monsterName = mViewModel.getName().getValue();
|
||||||
Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monsterName), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_added_to_dashboard, monsterName));
|
||||||
}
|
}
|
||||||
}, Logger::logError);
|
}, Logger::logError);
|
||||||
}
|
}
|
||||||
@@ -276,7 +291,7 @@ public class MonsterDetailFragment extends MCFragment {
|
|||||||
if (collections.isEmpty()) {
|
if (collections.isEmpty()) {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
Snackbar.make(view, getString(R.string.no_collections_available), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(view, getString(R.string.no_collections_available));
|
||||||
}
|
}
|
||||||
dispose();
|
dispose();
|
||||||
return;
|
return;
|
||||||
@@ -315,11 +330,7 @@ public class MonsterDetailFragment extends MCFragment {
|
|||||||
View view = getView();
|
View view = getView();
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
String monsterName = mViewModel.getName().getValue();
|
String monsterName = mViewModel.getName().getValue();
|
||||||
Snackbar.make(
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_monster_added_to_collection, monsterName, collection.name));
|
||||||
view,
|
|
||||||
getString(R.string.snackbar_monster_added_to_collection, monsterName, collection.name),
|
|
||||||
Snackbar.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import androidx.navigation.NavController;
|
|||||||
import androidx.navigation.NavDirections;
|
import androidx.navigation.NavDirections;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
||||||
import com.majinnaibu.monstercards.R;
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
@@ -34,6 +33,7 @@ import com.majinnaibu.monstercards.models.Monster;
|
|||||||
import com.majinnaibu.monstercards.ui.library.LibraryFragmentDirections;
|
import com.majinnaibu.monstercards.ui.library.LibraryFragmentDirections;
|
||||||
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
import com.majinnaibu.monstercards.ui.shared.MCFragment;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -174,7 +174,7 @@ public class MonsterImportFragment extends MCFragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
||||||
Logger.logError("Error creating monster", e);
|
Logger.logError("Error creating monster", e);
|
||||||
Snackbar.make(mHolder.root, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
|
SnackbarHelper.showLong(mHolder.root, getString(R.string.snackbar_failed_to_create_monster));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,15 +1,252 @@
|
|||||||
package com.majinnaibu.monstercards.ui.shared;
|
package com.majinnaibu.monstercards.ui.shared;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
|
import com.majinnaibu.monstercards.MainActivity;
|
||||||
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
||||||
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||||
|
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
||||||
|
import com.majinnaibu.monstercards.importers.Open5eImporter;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
|
import com.majinnaibu.monstercards.utils.ToastHelper;
|
||||||
|
|
||||||
|
import com.majinnaibu.monstercards.data.ImportConfig;
|
||||||
|
import com.majinnaibu.monstercards.models.ImportSource;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
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.schedulers.Schedulers;
|
||||||
|
|
||||||
public class MCFragment extends Fragment {
|
public class MCFragment extends Fragment {
|
||||||
|
private String mPendingExportContent;
|
||||||
|
private ActivityResultLauncher<Intent> mCreateDocumentLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> mOpenDocumentLauncher;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
mCreateDocumentLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
Uri uri = result.getData().getData();
|
||||||
|
if (uri != null && mPendingExportContent != null) {
|
||||||
|
writeContentToUri(uri, mPendingExportContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mPendingExportContent = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
mOpenDocumentLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
Intent data = result.getData();
|
||||||
|
List<Uri> uris = new ArrayList<>();
|
||||||
|
if (data.getClipData() != null) {
|
||||||
|
ClipData clipData = data.getClipData();
|
||||||
|
for (int i = 0; i < clipData.getItemCount(); i++) {
|
||||||
|
Uri uri = clipData.getItemAt(i).getUri();
|
||||||
|
if (uri != null) {
|
||||||
|
uris.add(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.getData() != null) {
|
||||||
|
uris.add(data.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uris.isEmpty() && getActivity() instanceof MainActivity) {
|
||||||
|
((MainActivity) getActivity()).importMultipleFilesFromUris(uris);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importMonsterFromFile() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
|
try {
|
||||||
|
mOpenDocumentLauncher.launch(intent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to launch file picker", e);
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.showLong(view, R.string.failed_to_import_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importCollectionFromFile() {
|
||||||
|
importMonsterFromFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showImportDialog() {
|
||||||
|
Context context = requireContext();
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
builder.setTitle(R.string.dialog_import_title);
|
||||||
|
|
||||||
|
View view = getLayoutInflater().inflate(R.layout.dialog_import, null);
|
||||||
|
builder.setView(view);
|
||||||
|
|
||||||
|
AlertDialog dialog = builder.create();
|
||||||
|
|
||||||
|
final EditText input = view.findViewById(R.id.edit_text_url);
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
if (clipboard != null && clipboard.hasPrimaryClip()) {
|
||||||
|
ClipData clip = clipboard.getPrimaryClip();
|
||||||
|
if (clip != null && clip.getItemCount() > 0) {
|
||||||
|
CharSequence clipText = clip.getItemAt(0).getText();
|
||||||
|
if (clipText != null) {
|
||||||
|
String clipStr = clipText.toString().trim();
|
||||||
|
if (new DnDBeyondImporter().canImport(clipStr) || new Open5eImporter().canImport(clipStr)) {
|
||||||
|
input.setText(clipStr);
|
||||||
|
input.selectAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById(R.id.button_import_url).setOnClickListener(v -> {
|
||||||
|
String urlOrId = input.getText().toString().trim();
|
||||||
|
if (!urlOrId.isEmpty()) {
|
||||||
|
if (getActivity() instanceof MainActivity) {
|
||||||
|
((MainActivity) getActivity()).importMonsterFromInputAndNavigate(urlOrId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.button_import_file).setOnClickListener(v -> {
|
||||||
|
importMonsterFromFile();
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.button_import_source).setOnClickListener(v -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
showSourcePicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSourcePicker() {
|
||||||
|
Context context = requireContext();
|
||||||
|
List<ImportSource> sources = ImportConfig.SOURCES;
|
||||||
|
if (sources.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] repoNames = new String[sources.size()];
|
||||||
|
for (int i = 0; i < sources.size(); i++) {
|
||||||
|
repoNames[i] = sources.get(i).projectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle(R.string.dialog_select_import_source)
|
||||||
|
.setItems(repoNames, (d, which) -> {
|
||||||
|
ImportSource selected = sources.get(which);
|
||||||
|
if (getActivity() instanceof MainActivity) {
|
||||||
|
((MainActivity) getActivity()).startImportFromSource(selected);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createNewMonster() {
|
||||||
|
Monster monster = new Monster();
|
||||||
|
monster.name = getString(R.string.default_monster_name);
|
||||||
|
MonsterRepository repository = getMonsterRepository();
|
||||||
|
repository.addMonster(monster)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(new DisposableCompletableObserver() {
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
navigateToEditMonster(monster.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
|
||||||
|
Logger.logError("Error creating monster", e);
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void navigateToEditMonster(@NonNull UUID monsterId) {
|
||||||
|
try {
|
||||||
|
NavController navController = Navigation.findNavController(requireView());
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putString("monster_id", monsterId.toString());
|
||||||
|
navController.navigate(R.id.edit_monster_navigation, args);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Error navigating to edit monster", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readContentsFromUri(@NonNull Uri uri) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return null;
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
builder.append(line).append("\n");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Error reading URI contents for import", e);
|
||||||
|
Context ctx = getContext();
|
||||||
|
if (ctx != null) {
|
||||||
|
ToastHelper.showLong(ctx, "An error occurred while reading the file.");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public MonsterCardsApplication getApplication() {
|
public MonsterCardsApplication getApplication() {
|
||||||
return (MonsterCardsApplication) requireActivity().getApplication();
|
return (MonsterCardsApplication) requireActivity().getApplication();
|
||||||
}
|
}
|
||||||
@@ -32,4 +269,76 @@ public class MCFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void exportToFile(@NonNull String defaultFileName, @NonNull String content) {
|
||||||
|
mPendingExportContent = content;
|
||||||
|
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
intent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
Uri downloadsUri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload");
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, downloadsUri);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mCreateDocumentLauncher.launch(intent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to launch document creation picker", e);
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.showLong(view, R.string.snackbar_export_failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeContentToUri(@NonNull Uri uri, @NonNull String content) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
try (OutputStream out = context.getContentResolver().openOutputStream(uri)) {
|
||||||
|
if (out != null) {
|
||||||
|
out.write(content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
String fileName = getFileNameFromUri(context, uri);
|
||||||
|
String message = getString(R.string.snackbar_export_success, fileName);
|
||||||
|
SnackbarHelper.showLong(view, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Failed to write export content to URI", e);
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
SnackbarHelper.showLong(view, R.string.snackbar_export_failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileNameFromUri(@NonNull Context context, @NonNull Uri uri) {
|
||||||
|
String displayName = null;
|
||||||
|
if ("content".equals(uri.getScheme())) {
|
||||||
|
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
|
if (nameIndex != -1) {
|
||||||
|
displayName = cursor.getString(nameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logError("Error querying display name from content URI", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return displayName != null ? displayName : defaultFileNameFromUri(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultFileNameFromUri(@NonNull Uri uri) {
|
||||||
|
String path = uri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
int lastSlash = path.lastIndexOf('/');
|
||||||
|
if (lastSlash >= 0 && lastSlash < path.length() - 1) {
|
||||||
|
return path.substring(lastSlash + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uri.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.majinnaibu.monstercards.utils;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized helper for managing Snackbar notifications and durations across the app.
|
||||||
|
*/
|
||||||
|
public class SnackbarHelper {
|
||||||
|
// Configurable default durations in milliseconds
|
||||||
|
public static int DEFAULT_SHORT_DURATION = 2500; // 2.5 seconds
|
||||||
|
public static int DEFAULT_LONG_DURATION = 4000; // 4.0 seconds (e.g. for Undo or action messages)
|
||||||
|
|
||||||
|
public static Snackbar makeShort(@NonNull View view, @StringRes int resId) {
|
||||||
|
return make(view, view.getContext().getString(resId), DEFAULT_SHORT_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar makeShort(@NonNull View view, @NonNull CharSequence message) {
|
||||||
|
return make(view, message, DEFAULT_SHORT_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar makeLong(@NonNull View view, @StringRes int resId) {
|
||||||
|
return make(view, view.getContext().getString(resId), DEFAULT_LONG_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar makeLong(@NonNull View view, @NonNull CharSequence message) {
|
||||||
|
return make(view, message, DEFAULT_LONG_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar makeIndefinite(@NonNull View view, @StringRes int resId) {
|
||||||
|
return Snackbar.make(view, resId, Snackbar.LENGTH_INDEFINITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar makeIndefinite(@NonNull View view, @NonNull CharSequence message) {
|
||||||
|
return Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar make(@NonNull View view, @NonNull CharSequence message, int durationMs) {
|
||||||
|
return Snackbar.make(view, message, durationMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showShort(@NonNull View view, @StringRes int resId) {
|
||||||
|
makeShort(view, resId).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showShort(@NonNull View view, @NonNull CharSequence message) {
|
||||||
|
makeShort(view, message).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showLong(@NonNull View view, @StringRes int resId) {
|
||||||
|
makeLong(view, resId).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showLong(@NonNull View view, @NonNull CharSequence message) {
|
||||||
|
makeLong(view, message).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.majinnaibu.monstercards.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized helper for managing Toast notifications and durations across the app.
|
||||||
|
*/
|
||||||
|
public class ToastHelper {
|
||||||
|
// Configurable default durations
|
||||||
|
public static int DEFAULT_SHORT_DURATION = Toast.LENGTH_SHORT; // ~2.0 seconds
|
||||||
|
public static int DEFAULT_LONG_DURATION = Toast.LENGTH_LONG; // ~3.5 seconds
|
||||||
|
|
||||||
|
private static Toast sCurrentToast;
|
||||||
|
|
||||||
|
public static void showShort(@NonNull Context context, @StringRes int resId) {
|
||||||
|
show(context, context.getString(resId), DEFAULT_SHORT_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showShort(@NonNull Context context, @NonNull String message) {
|
||||||
|
show(context, message, DEFAULT_SHORT_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showLong(@NonNull Context context, @StringRes int resId) {
|
||||||
|
show(context, context.getString(resId), DEFAULT_LONG_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showLong(@NonNull Context context, @NonNull String message) {
|
||||||
|
show(context, message, DEFAULT_LONG_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(@NonNull Context context, @NonNull String message, int duration) {
|
||||||
|
if (sCurrentToast != null) {
|
||||||
|
sCurrentToast.cancel();
|
||||||
|
}
|
||||||
|
sCurrentToast = Toast.makeText(context.getApplicationContext(), message, duration);
|
||||||
|
sCurrentToast.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Android/app/src/main/res/drawable/ic_expand_less_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
|
||||||
|
</vector>
|
||||||
10
Android/app/src/main/res/drawable/ic_expand_more_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
|
||||||
|
</vector>
|
||||||
@@ -5,166 +5,6 @@
|
|||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#3DDC84"
|
android:fillColor="@color/colorPrimaryDark"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
BIN
Android/app/src/main/res/drawable/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
Android/app/src/main/res/drawable/ic_logo.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
Android/app/src/main/res/drawable/ic_splash_combined.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
Android/app/src/main/res/drawable/ic_splash_logo.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Solid dark red background -->
|
||||||
|
<item android:drawable="@color/colorPrimaryDark" />
|
||||||
|
|
||||||
|
<!-- Centered logo + Spindle's End font text -->
|
||||||
|
<item
|
||||||
|
android:width="280dp"
|
||||||
|
android:height="280dp"
|
||||||
|
android:drawable="@drawable/ic_splash_combined"
|
||||||
|
android:gravity="center" />
|
||||||
|
</layer-list>
|
||||||
BIN
Android/app/src/main/res/font/spindles_end.ttf
Normal file
@@ -3,7 +3,27 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/container"
|
android:id="@+id/container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/app_bar_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Light"
|
||||||
|
app:navigationIconTint="@android:color/white"
|
||||||
|
app:titleTextColor="@android:color/white" />
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
android:id="@+id/nav_view"
|
android:id="@+id/nav_view"
|
||||||
@@ -11,7 +31,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="0dp"
|
android:layout_marginStart="0dp"
|
||||||
android:layout_marginEnd="0dp"
|
android:layout_marginEnd="0dp"
|
||||||
android:background="?android:attr/windowBackground"
|
android:background="?attr/colorSurface"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
@@ -19,7 +39,6 @@
|
|||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/nav_host_fragment"
|
android:id="@+id/nav_host_fragment"
|
||||||
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
@@ -28,8 +47,8 @@
|
|||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toBottomOf="@id/app_bar_layout"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0"
|
||||||
app:navGraph="@navigation/mobile_navigation" />
|
app:navGraph="@navigation/mobile_navigation" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
34
Android/app/src/main/res/layout/activity_splash.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/colorPrimaryDark">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/splash_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:fontFamily="@font/spindles_end"
|
||||||
|
android:text="@string/app_title_splash"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/splash_logo"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/splash_logo"
|
||||||
|
android:layout_width="220dp"
|
||||||
|
android:layout_height="220dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_logo" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
android:id="@+id/decrement"
|
android:id="@+id/decrement"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
android:paddingVertical="8dp"
|
android:paddingVertical="8dp"
|
||||||
android:text="@string/label_decrement_field"
|
android:text="@string/label_decrement_field"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|||||||
50
Android/app/src/main/res/layout/dialog_import.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/dialog_import_message"
|
||||||
|
android:paddingBottom="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_url"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/dialog_import_url_hint"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:singleLine="true" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_import_url"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/action_import_from_url" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="?android:attr/listDivider" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_import_file"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/action_import_monster_from_file" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_import_source"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/action_import_from_source" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -17,6 +17,56 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:listitem="@layout/collection_list_item" />
|
tools:listitem="@layout/collection_list_item" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/empty_state"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/empty_state_logo"
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="140dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
app:srcCompat="@drawable/ic_logo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_state_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/empty_collections_title"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_state_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/empty_collections_subtitle"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/empty_state_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/action_add_collection"
|
||||||
|
app:cornerRadius="20dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab_add_collection"
|
android:id="@+id/fab_add_collection"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -21,6 +21,56 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:listitem="@layout/card_monster" />
|
tools:listitem="@layout/card_monster" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/empty_state"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/empty_state_logo"
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="140dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
app:srcCompat="@drawable/ic_logo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_state_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/empty_dashboard_title"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_state_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/empty_dashboard_subtitle"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/empty_state_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/action_add_to_dashboard"
|
||||||
|
app:cornerRadius="20dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab_add_to_dashboard"
|
android:id="@+id/fab_add_to_dashboard"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.editmonster.EditBasicInfoFragment">
|
android:clipToPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
|
tools:context=".ui.editmonster.EditAbilityScoresFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.editmonster.EditBasicInfoFragment">
|
android:clipToPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
|
tools:context=".ui.editmonster.EditArmorFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
tools:context=".ui.editmonster.EditBasicInfoFragment">
|
tools:context=".ui.editmonster.EditBasicInfoFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
tools:context=".ui.editmonster.EditChallengeRatingFragment">
|
tools:context=".ui.editmonster.EditChallengeRatingFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -3,187 +3,545 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:divider="?android:attr/dividerVertical"
|
|
||||||
android:dividerPadding="@dimen/text_margin"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:showDividers="middle"
|
|
||||||
tools:context=".ui.editmonster.EditMonsterFragment">
|
tools:context=".ui.editmonster.EditMonsterFragment">
|
||||||
|
|
||||||
<TextView
|
<!-- Card 1: Basic Info & Challenge Rating -->
|
||||||
android:id="@+id/basicInfo"
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardBasicInfo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="@string/label_basic_info"
|
app:cardCornerRadius="8dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
app:cardElevation="2dp">
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/armor"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:orientation="vertical">
|
||||||
android:layout_margin="@dimen/text_margin"
|
|
||||||
android:text="@string/label_armor"
|
|
||||||
android:textSize="@dimen/text_h4_size"
|
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
|
||||||
|
|
||||||
<TextView
|
<RelativeLayout
|
||||||
android:id="@+id/speed"
|
android:id="@+id/headerBasicInfo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:text="@string/label_speed"
|
android:clickable="true"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:focusable="true"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:padding="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/abilityScores"
|
android:id="@+id/titleBasicInfo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_alignParentStart="true"
|
||||||
android:text="@string/label_ability_scores"
|
android:text="Basic Info & Challenge Rating"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/savingThrows"
|
android:id="@+id/summaryBasicInfo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_below="@id/titleBasicInfo"
|
||||||
android:text="@string/label_saving_throws"
|
android:layout_alignParentStart="true"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:layout_marginTop="4dp"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:layout_toStartOf="@+id/chevronBasicInfo"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="Goblin — Small humanoid, CR 1/4" />
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/skills"
|
android:id="@+id/chevronBasicInfo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="24dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="24dp"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_alignParentEnd="true"
|
||||||
android:text="@string/label_skills"
|
android:layout_centerVertical="true"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:contentDescription="Expand/Collapse"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:src="@drawable/ic_expand_more_24" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/conditionImmunities"
|
android:id="@+id/bodyBasicInfo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:divider="?android:attr/dividerVertical"
|
||||||
android:text="@string/label_condition_immunities"
|
android:dividerPadding="8dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:orientation="vertical"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:showDividers="middle"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/damageImmunities"
|
android:id="@+id/basicInfo"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:text="@string/label_damage_immunities"
|
android:padding="16dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:text="@string/label_basic_info"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/damageResistances"
|
android:id="@+id/challengeRating"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:text="@string/label_damage_resistances"
|
android:padding="16dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:text="@string/label_challenge_rating"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<TextView
|
<!-- Card 2: Armor, Speed & Ability Scores -->
|
||||||
android:id="@+id/damageVulnerabilities"
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardCombatAbilities"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="@string/label_damage_vulnerabilities"
|
app:cardCornerRadius="8dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
app:cardElevation="2dp">
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/senses"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:orientation="vertical">
|
||||||
android:layout_margin="@dimen/text_margin"
|
|
||||||
android:text="@string/label_senses"
|
|
||||||
android:textSize="@dimen/text_h4_size"
|
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
|
||||||
|
|
||||||
<TextView
|
<RelativeLayout
|
||||||
android:id="@+id/languages"
|
android:id="@+id/headerCombatAbilities"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:text="@string/label_languages"
|
android:clickable="true"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:focusable="true"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:padding="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/challengeRating"
|
android:id="@+id/titleCombatAbilities"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_alignParentStart="true"
|
||||||
android:text="@string/label_challenge_rating"
|
android:text="Armor, Speed & Ability Scores"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/abilities"
|
android:id="@+id/summaryCombatAbilities"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_below="@id/titleCombatAbilities"
|
||||||
android:text="@string/label_abilities"
|
android:layout_alignParentStart="true"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:layout_marginTop="4dp"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:layout_toStartOf="@+id/chevronCombatAbilities"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="STR 8 | DEX 14 | CON 10 | INT 10 | WIS 14 | CHA 15" />
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/actions"
|
android:id="@+id/chevronCombatAbilities"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="24dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="24dp"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_alignParentEnd="true"
|
||||||
android:text="@string/label_actions"
|
android:layout_centerVertical="true"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:contentDescription="Expand/Collapse"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:src="@drawable/ic_expand_more_24" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/reactions"
|
android:id="@+id/bodyCombatAbilities"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:divider="?android:attr/dividerVertical"
|
||||||
android:text="@string/label_reactions"
|
android:dividerPadding="8dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:orientation="vertical"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:showDividers="middle"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/legendaryActions"
|
android:id="@+id/armor"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:text="@string/label_legendary_actions"
|
android:padding="16dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:text="@string/label_armor"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/lairActions"
|
android:id="@+id/speed"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:text="@string/label_lair_actions"
|
android:padding="16dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
android:text="@string/label_speed"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/regionalActions"
|
android:id="@+id/abilityScores"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_ability_scores"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Card 3: Saves, Skills & Languages -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardSavesSkillsLanguages"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/text_margin"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="@string/label_regional_effects"
|
app:cardCornerRadius="8dp"
|
||||||
android:textSize="@dimen/text_h4_size"
|
app:cardElevation="2dp">
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/headerSavesSkillsLanguages"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleSavesSkillsLanguages"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:text="Saves, Skills & Languages"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/summarySavesSkillsLanguages"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/titleSavesSkillsLanguages"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_toStartOf="@+id/chevronSavesSkillsLanguages"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="3 Saves, 2 Skills, Common & Goblin" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/chevronSavesSkillsLanguages"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:contentDescription="Expand/Collapse"
|
||||||
|
android:src="@drawable/ic_expand_more_24" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bodySavesSkillsLanguages"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:divider="?android:attr/dividerVertical"
|
||||||
|
android:dividerPadding="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:showDividers="middle"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/savingThrows"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_saving_throws"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/skills"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_skills"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/senses"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_senses"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/languages"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_languages"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Card 4: Defenses & Immunities -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardDefenses"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/headerDefenses"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleDefenses"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:text="Defenses & Immunities"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/summaryDefenses"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/titleDefenses"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_toStartOf="@+id/chevronDefenses"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="Poison Immunity, Fire Resistance" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/chevronDefenses"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:contentDescription="Expand/Collapse"
|
||||||
|
android:src="@drawable/ic_expand_more_24" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bodyDefenses"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:divider="?android:attr/dividerVertical"
|
||||||
|
android:dividerPadding="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:showDividers="middle"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/conditionImmunities"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_condition_immunities"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/damageImmunities"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_damage_immunities"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/damageResistances"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_damage_resistances"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/damageVulnerabilities"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_damage_vulnerabilities"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Card 5: Abilities, Actions & Traits -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardTraitsActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/headerTraitsActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleTraitsActions"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:text="Abilities, Actions & Traits"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/summaryTraitsActions"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/titleTraitsActions"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_toStartOf="@+id/chevronTraitsActions"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="2 Abilities, 2 Actions, 1 Reaction" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/chevronTraitsActions"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:contentDescription="Expand/Collapse"
|
||||||
|
android:src="@drawable/ic_expand_more_24" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bodyTraitsActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:divider="?android:attr/dividerVertical"
|
||||||
|
android:dividerPadding="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:showDividers="middle"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/abilities"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_abilities"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/actions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_actions"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reactions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_reactions"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/legendaryActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_legendary_actions"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/lairActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_lair_actions"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/regionalActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/label_regional_effects"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_chevron_right_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
tools:context=".ui.editmonster.EditSavingThrowsFragment">
|
tools:context=".ui.editmonster.EditSavingThrowsFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.editmonster.EditBasicInfoFragment">
|
android:clipToPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
|
tools:context=".ui.editmonster.EditSpeedFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -19,6 +19,56 @@
|
|||||||
tools:context=".MonsterListFragment"
|
tools:context=".MonsterListFragment"
|
||||||
tools:listitem="@layout/monster_list_content" />
|
tools:listitem="@layout/monster_list_content" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/empty_state"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/empty_state_logo"
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="140dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
app:srcCompat="@drawable/ic_logo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_state_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/empty_library_title"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_state_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/empty_library_subtitle"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/empty_state_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/action_add_monster"
|
||||||
|
app:cornerRadius="20dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -6,4 +6,14 @@
|
|||||||
android:id="@+id/menu_action_add_collection_to_dashboard"
|
android:id="@+id/menu_action_add_collection_to_dashboard"
|
||||||
android:title="@string/action_add_collection_to_dashboard"
|
android:title="@string/action_add_collection_to_dashboard"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_collection"
|
||||||
|
android:title="@string/action_export_collection"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_remove_all_monsters_from_collection"
|
||||||
|
android:title="@string/action_remove_all"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
14
Android/app/src/main/res/menu/collections_menu.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_collections"
|
||||||
|
android:title="@string/action_export"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_remove_all_collections"
|
||||||
|
android:title="@string/action_remove_all"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
||||||
@@ -16,4 +16,9 @@
|
|||||||
android:id="@+id/menu_action_clear_dashboard"
|
android:id="@+id/menu_action_clear_dashboard"
|
||||||
android:title="@string/action_clear_dashboard"
|
android:title="@string/action_clear_dashboard"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_dashboard"
|
||||||
|
android:title="@string/action_export_dashboard"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
<item
|
<item
|
||||||
android:id="@+id/menu_action_import_monster"
|
android:id="@+id/menu_action_import_monster"
|
||||||
android:icon="@drawable/ic_library_add_24"
|
android:icon="@drawable/ic_library_add_24"
|
||||||
android:title="@string/action_import_monster"
|
android:title="@string/action_import"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -3,7 +3,22 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_action_import_from_url"
|
android:id="@+id/menu_action_import"
|
||||||
android:title="@string/action_import_from_url"
|
android:title="@string/action_import"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_library"
|
||||||
|
android:title="@string/action_export_library"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_everything"
|
||||||
|
android:title="@string/action_export_everything"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_clear_all_data"
|
||||||
|
android:title="@string/action_clear_all_data"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -23,4 +23,9 @@
|
|||||||
android:id="@+id/menu_action_add_to_dashboard"
|
android:id="@+id/menu_action_add_to_dashboard"
|
||||||
android:title="@string/action_add_to_dashboard"
|
android:title="@string/action_add_to_dashboard"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_card"
|
||||||
|
android:title="@string/action_export_card"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 42 KiB |
25
Android/app/src/main/res/values-v31/styles.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Base application theme for Android 12+ (API 31+). -->
|
||||||
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorSecondary">@color/colorSecondary</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
<item name="colorOnPrimary">#ffffff</item>
|
||||||
|
<item name="colorOnSecondary">#000000</item>
|
||||||
|
|
||||||
|
<item name="android:windowBackground">@color/colorPrimaryDark</item>
|
||||||
|
|
||||||
|
<!-- Android 12+ Splash Screen API -->
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_splash_logo</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@color/colorPrimaryDark</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.App.Starting" parent="AppTheme">
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_splash_logo</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@color/colorPrimaryDark</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<string name="action_add_skill">Add Skill</string>
|
<string name="action_add_skill">Add Skill</string>
|
||||||
<string name="action_add_trait">Add Trait</string>
|
<string name="action_add_trait">Add Trait</string>
|
||||||
<string name="action_edit">Edit</string>
|
<string name="action_edit">Edit</string>
|
||||||
<string name="action_import_monster">Import Monster</string>
|
<string name="action_import">Import</string>
|
||||||
<string name="app_name">MonsterCards</string>
|
<string name="app_name">MonsterCards</string>
|
||||||
<string name="charisma_abbreviation">CHA</string>
|
<string name="charisma_abbreviation">CHA</string>
|
||||||
<string name="constitution_abbreviation">CON</string>
|
<string name="constitution_abbreviation">CON</string>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<string name="title_editStrings">Strings</string>
|
<string name="title_editStrings">Strings</string>
|
||||||
<string name="title_editTrait">Trait</string>
|
<string name="title_editTrait">Trait</string>
|
||||||
<string name="title_editTraits">Traits</string>
|
<string name="title_editTraits">Traits</string>
|
||||||
<string name="title_importMonster">Import Monster</string>
|
<string name="title_importMonster">Import File</string>
|
||||||
<string name="title_library">Library</string>
|
<string name="title_library">Library</string>
|
||||||
<string name="title_monsterDetails">Monster Details</string>
|
<string name="title_monsterDetails">Monster Details</string>
|
||||||
<string name="title_monsterDetails_fmt">%1$s Details</string>
|
<string name="title_monsterDetails_fmt">%1$s Details</string>
|
||||||
@@ -156,19 +156,84 @@
|
|||||||
<string name="action_add_collection_to_dashboard">Add Collection to Dashboard</string>
|
<string name="action_add_collection_to_dashboard">Add Collection to Dashboard</string>
|
||||||
<string name="action_add_single_monster">Add Monster</string>
|
<string name="action_add_single_monster">Add Monster</string>
|
||||||
<string name="action_add_collection_option">Add Collection</string>
|
<string name="action_add_collection_option">Add Collection</string>
|
||||||
<string name="action_clear_dashboard">Clear Dashboard</string>
|
<string name="action_clear_dashboard">Clear</string>
|
||||||
<string name="dialog_add_to_dashboard">Add to Dashboard</string>
|
<string name="dialog_add_to_dashboard">Add to Dashboard</string>
|
||||||
<string name="snackbar_added_to_dashboard">Added %1$s to Dashboard</string>
|
<string name="snackbar_added_to_dashboard">Added %1$s to Dashboard</string>
|
||||||
<string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
|
<string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
|
||||||
<string name="snackbar_dashboard_cleared">Dashboard cleared</string>
|
<string name="snackbar_dashboard_cleared">Dashboard cleared</string>
|
||||||
<string name="no_monsters_available">No monsters available in library. Create one first!</string>
|
<string name="no_monsters_available">No monsters available in library. Create one first!</string>
|
||||||
<string name="action_import_from_url">Import URL...</string>
|
<string name="action_import_from_url">Import URL</string>
|
||||||
<string name="dialog_import_url_title">Import Entity or Character</string>
|
|
||||||
<string name="dialog_import_url_message">Enter or paste a D&D Beyond URL, Character ID, or Open5e JSON payload:</string>
|
<string name="dialog_import_title">Import</string>
|
||||||
|
<string name="dialog_import_message">Enter or paste a URL, ID, or data payload:</string>
|
||||||
<string name="dialog_import_url_hint">e.g. D&D Beyond URL or Open5e JSON</string>
|
<string name="dialog_import_url_hint">e.g. D&D Beyond URL or Open5e JSON</string>
|
||||||
<string name="dialog_import">Import</string>
|
<string name="dialog_import">Import</string>
|
||||||
<string name="toast_importing_url">Importing entity...</string>
|
<string name="toast_importing_url">Importing…</string>
|
||||||
<string name="failed_to_import_url">Failed to import entity. Please check the URL, ID, or JSON format.</string>
|
<string name="failed_to_import_url">Failed to import URL. Please check the URL, ID, or JSON format.</string>
|
||||||
<string name="action_share_monster">Share / Export Monster (.card)</string>
|
<string name="action_share_monster">Share / Export Monster (.card)</string>
|
||||||
<string name="failed_to_share_monster">Failed to share monster</string>
|
<string name="failed_to_share_monster">Failed to share monster</string>
|
||||||
|
<string name="action_export_card">Export Card</string>
|
||||||
|
<string name="action_export_collection">Export Collection</string>
|
||||||
|
<string name="action_export_library">Export Library</string>
|
||||||
|
<string name="action_export_dashboard">Export</string>
|
||||||
|
<string name="snackbar_export_success">Exported to %1$s</string>
|
||||||
|
<string name="snackbar_export_failed">Failed to export file</string>
|
||||||
|
<string name="snackbar_import_binder_success">Imported binder successfully</string>
|
||||||
|
<string name="default_filename_library">MonsterCards Library</string>
|
||||||
|
<string name="default_filename_dashboard">MonsterCards Dashboard</string>
|
||||||
|
|
||||||
|
<string name="empty_dashboard_title">Dashboard is Empty</string>
|
||||||
|
<string name="empty_dashboard_subtitle">Pin monster cards or collections here for quick reference during your tabletop sessions.</string>
|
||||||
|
<string name="empty_collections_title">No Collections Yet</string>
|
||||||
|
<string name="empty_collections_subtitle">Organize your monsters into custom campaign or encounter collections.</string>
|
||||||
|
<string name="empty_library_title">Your Library is Empty</string>
|
||||||
|
<string name="empty_library_subtitle">Create new monster cards or import them from D&D Beyond or Open5e.</string>
|
||||||
|
<string name="app_title_splash">Monster Cards</string>
|
||||||
|
|
||||||
|
<string name="action_remove_single_monster">Remove Monster</string>
|
||||||
|
<string name="action_remove">Remove</string>
|
||||||
|
<string name="action_undo">Undo</string>
|
||||||
|
<string name="action_view_details">View Details</string>
|
||||||
|
<string name="action_remove_from_dashboard">Remove from Dashboard</string>
|
||||||
|
<string name="snackbar_removed_from_dashboard">Removed %1$s from Dashboard</string>
|
||||||
|
<string name="snackbar_dashboard_already_empty">Dashboard is already empty</string>
|
||||||
|
<string name="dialog_remove_from_dashboard_title">Remove %1$s?</string>
|
||||||
|
<string name="dialog_remove_from_dashboard_message">Remove %1$s from your dashboard?</string>
|
||||||
|
|
||||||
|
<string name="action_create_monster">Create Monster</string>
|
||||||
|
<string name="action_import_monster_from_url">Import URL</string>
|
||||||
|
<string name="action_import_monster_from_file">Import File</string>
|
||||||
|
<string name="action_import_collection">Import File</string>
|
||||||
|
<string name="title_library_actions">Library Actions</string>
|
||||||
|
<string name="title_collection_actions">Collection Actions</string>
|
||||||
|
<string name="title_dashboard_actions">Dashboard Actions</string>
|
||||||
|
|
||||||
|
<string name="action_clear">Clear</string>
|
||||||
|
<string name="action_clear_all_data">Clear All Data</string>
|
||||||
|
<string name="dialog_clear_all_data_title">Clear All Data?</string>
|
||||||
|
<string name="dialog_clear_all_data_message">This will permanently delete all monsters, collections, and clear your dashboard. Are you sure?</string>
|
||||||
|
<string name="snackbar_all_data_cleared">All data cleared</string>
|
||||||
|
|
||||||
|
<string name="action_export_everything">Export All Data</string>
|
||||||
|
<string name="default_filename_export_everything">monster_cards_backup</string>
|
||||||
|
|
||||||
|
<string name="action_export">Export</string>
|
||||||
|
<string name="action_remove_all">Remove All</string>
|
||||||
|
<string name="dialog_remove_all_collections_title">Remove All Collections?</string>
|
||||||
|
<string name="dialog_remove_all_collections_message">Are you sure you want to remove all collections? Monsters in your library will not be deleted.</string>
|
||||||
|
<string name="snackbar_all_collections_removed">All collections removed</string>
|
||||||
|
|
||||||
|
<string name="dialog_remove_all_from_collection_title">Remove All Monsters?</string>
|
||||||
|
<string name="dialog_remove_all_from_collection_message">Are you sure you want to remove all monsters from this collection?</string>
|
||||||
|
<string name="snackbar_collection_cleared">Removed all monsters from collection</string>
|
||||||
|
<string name="action_import_from_source">Import from Source Project</string>
|
||||||
|
<string name="dialog_select_import_source">Select Import Source</string>
|
||||||
|
<string name="snackbar_importing_source">Importing from %1$s…</string>
|
||||||
|
<string name="snackbar_import_source_complete">Finished importing %1$d monsters from %2$s.</string>
|
||||||
|
<string name="snackbar_import_source_failed">Failed to import from %1$s.</string>
|
||||||
|
<string name="snackbar_import_source_cancelled">Cancelled import from %1$s.</string>
|
||||||
|
<string name="dialog_import_already_running_title">Import in Progress</string>
|
||||||
|
<string name="dialog_import_already_running_message">An import from %1$s is currently running. Do you want to cancel it and start a new import?</string>
|
||||||
|
<string name="action_cancel_current">Cancel Current</string>
|
||||||
|
<string name="action_cancel">Cancel</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorSecondary">@color/colorSecondary</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
<item name="colorOnPrimary">#ffffff</item>
|
||||||
|
<item name="colorOnSecondary">#000000</item>
|
||||||
|
|
||||||
|
<item name="android:windowBackground">@color/colorPrimaryDark</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Instant starting theme displaying logo on launch -->
|
||||||
|
<style name="Theme.App.Starting" parent="AppTheme">
|
||||||
|
<item name="android:windowBackground">@color/colorPrimaryDark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||