Adds option to import from Open5e.com
This commit is contained in:
@@ -21,16 +21,20 @@ import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.navigation.ui.AppBarConfiguration;
|
||||
import androidx.navigation.ui.NavigationUI;
|
||||
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.gson.Gson;
|
||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||
import com.majinnaibu.monstercards.importers.BinderImporter;
|
||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
||||
import com.majinnaibu.monstercards.importers.Open5eApiWrapper;
|
||||
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
||||
import com.majinnaibu.monstercards.models.BinderExport;
|
||||
import com.majinnaibu.monstercards.models.Monster;
|
||||
import com.majinnaibu.monstercards.utils.Logger;
|
||||
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||
import com.majinnaibu.monstercards.utils.ToastHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -44,10 +48,13 @@ import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private Disposable mOpen5eImportDisposable;
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
@@ -67,7 +74,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
AppCenterInitializer.init(getApplication());
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
com.google.android.material.appbar.MaterialToolbar toolbar = findViewById(R.id.toolbar);
|
||||
MaterialToolbar toolbar = findViewById(R.id.toolbar);
|
||||
if (toolbar != null) {
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
@@ -202,6 +209,55 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
public void importAllFromOpen5e() {
|
||||
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
||||
ToastHelper.showShort(this, "Open5e import already running");
|
||||
return;
|
||||
}
|
||||
|
||||
View rootView = findViewById(android.R.id.content);
|
||||
Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, R.string.snackbar_importing_open5e);
|
||||
snackbar.setAction(R.string.action_cancel, v -> {
|
||||
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
||||
mOpen5eImportDisposable.dispose();
|
||||
SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_cancelled);
|
||||
}
|
||||
});
|
||||
snackbar.show();
|
||||
|
||||
mOpen5eImportDisposable = Single.fromCallable(() -> {
|
||||
int totalImported = 0;
|
||||
String nextUrl = null;
|
||||
do {
|
||||
if (mOpen5eImportDisposable != null && mOpen5eImportDisposable.isDisposed()) {
|
||||
break;
|
||||
}
|
||||
Open5eApiWrapper.Open5ePageResult page = Open5eApiWrapper.fetchPage(nextUrl);
|
||||
for (Monster monster : page.monsters) {
|
||||
if (mOpen5eImportDisposable != null && mOpen5eImportDisposable.isDisposed()) {
|
||||
break;
|
||||
}
|
||||
((MonsterCardsApplication) getApplication()).getMonsterRepository()
|
||||
.saveMonster(monster)
|
||||
.blockingAwait();
|
||||
totalImported++;
|
||||
}
|
||||
nextUrl = page.nextUrl;
|
||||
} while (nextUrl != null && !nextUrl.isEmpty());
|
||||
return totalImported;
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
snackbar.dismiss();
|
||||
SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_complete);
|
||||
}, throwable -> {
|
||||
snackbar.dismiss();
|
||||
Logger.logError("Failed to import all Open5e monsters", throwable);
|
||||
SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_failed);
|
||||
});
|
||||
}
|
||||
|
||||
public void importMultipleFilesFromUris(@NonNull List<Uri> uris) {
|
||||
if (uris.isEmpty()) return;
|
||||
if (uris.size() == 1) {
|
||||
@@ -370,4 +426,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
||||
mOpen5eImportDisposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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) 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) {
|
||||
sb.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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")) {
|
||||
String speedDesc = getString(props, "speed_desc");
|
||||
if (!speedDesc.isEmpty()) {
|
||||
@@ -159,6 +168,10 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
||||
crStr = props.get("challenge_rating").getAsString();
|
||||
}
|
||||
if (!crStr.isEmpty()) {
|
||||
crStr = crStr.replace(".0", "");
|
||||
if (crStr.equals("0.125")) crStr = "1/8";
|
||||
else if (crStr.equals("0.25")) crStr = "1/4";
|
||||
else if (crStr.equals("0.5")) crStr = "1/2";
|
||||
monster.challengeRating = ChallengeRating.valueOfString(crStr);
|
||||
}
|
||||
}
|
||||
@@ -214,7 +227,12 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
||||
private void parseSavingThrows(JsonObject props, Monster monster) {
|
||||
if (!props.has("saving_throws")) return;
|
||||
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();
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
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) {
|
||||
if (!props.has("skills")) return;
|
||||
JsonElement skillsEl = props.get("skills");
|
||||
if (skillsEl.isJsonArray()) {
|
||||
JsonArray arr = skillsEl.getAsJsonArray();
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
String skillStr = arr.get(i).getAsString();
|
||||
String skillName = skillStr.replaceAll("[+-]\\d+", "").trim();
|
||||
if (!skillName.isEmpty()) {
|
||||
AbilityScore stat = getAbilityForSkill(skillName);
|
||||
monster.skills.add(new Skill(capitalize(skillName), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
|
||||
if (props.has("skill_bonuses") && props.get("skill_bonuses").isJsonObject()) {
|
||||
JsonObject skillsObj = props.getAsJsonObject("skill_bonuses");
|
||||
for (String key : skillsObj.keySet()) {
|
||||
AbilityScore stat = getAbilityForSkill(key);
|
||||
monster.skills.add(new Skill(capitalize(key.replace("_", " ")), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
|
||||
}
|
||||
} else if (props.has("skills")) {
|
||||
JsonElement skillsEl = props.get("skills");
|
||||
if (skillsEl.isJsonArray()) {
|
||||
JsonArray arr = skillsEl.getAsJsonArray();
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
String skillStr = arr.get(i).getAsString();
|
||||
String skillName = skillStr.replaceAll("[+-]\\d+", "").trim();
|
||||
if (!skillName.isEmpty()) {
|
||||
AbilityScore stat = getAbilityForSkill(skillName);
|
||||
monster.skills.add(new Skill(capitalize(skillName), stat, AdvantageType.NONE, ProficiencyType.PROFICIENT));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +299,26 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
||||
private void parseLanguages(JsonObject props, Monster monster) {
|
||||
if (props.has("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();
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
String langName = arr.get(i).getAsString();
|
||||
@@ -294,10 +338,18 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
||||
}
|
||||
|
||||
private void parseDamageAndConditions(JsonObject props, Monster monster) {
|
||||
parseStringSet(props, "damage_immunities", monster.damageImmunities);
|
||||
parseStringSet(props, "damage_resistances", monster.damageResistances);
|
||||
parseStringSet(props, "damage_vulnerabilities", monster.damageVulnerabilities);
|
||||
parseStringSet(props, "condition_immunities", monster.conditionImmunities);
|
||||
if (props.has("resistances_and_immunities") && props.get("resistances_and_immunities").isJsonObject()) {
|
||||
JsonObject resObj = props.getAsJsonObject("resistances_and_immunities");
|
||||
parseStringSet(resObj, "damage_immunities", monster.damageImmunities);
|
||||
parseStringSet(resObj, "damage_resistances", monster.damageResistances);
|
||||
parseStringSet(resObj, "damage_vulnerabilities", monster.damageVulnerabilities);
|
||||
parseStringSet(resObj, "condition_immunities", monster.conditionImmunities);
|
||||
} else {
|
||||
parseStringSet(props, "damage_immunities", monster.damageImmunities);
|
||||
parseStringSet(props, "damage_resistances", monster.damageResistances);
|
||||
parseStringSet(props, "damage_vulnerabilities", monster.damageVulnerabilities);
|
||||
parseStringSet(props, "condition_immunities", monster.conditionImmunities);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseStringSet(JsonObject props, String key, java.util.Set<String> set) {
|
||||
@@ -306,8 +358,14 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
||||
if (el.isJsonArray()) {
|
||||
JsonArray arr = el.getAsJsonArray();
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
String val = arr.get(i).getAsString();
|
||||
if (!val.isEmpty()) set.add(capitalize(val));
|
||||
JsonElement item = arr.get(i);
|
||||
if (item.isJsonObject() && item.getAsJsonObject().has("name")) {
|
||||
String val = item.getAsJsonObject().get("name").getAsString();
|
||||
if (!val.isEmpty()) set.add(capitalize(val));
|
||||
} else if (item.isJsonPrimitive()) {
|
||||
String val = item.getAsString();
|
||||
if (!val.isEmpty()) set.add(capitalize(val));
|
||||
}
|
||||
}
|
||||
} else if (el.isJsonPrimitive()) {
|
||||
for (String val : el.getAsString().split(",")) {
|
||||
@@ -357,7 +415,12 @@ public class Open5eImporter implements EntityImporter<Monster> {
|
||||
|
||||
private static String getString(JsonObject obj, String key, String defaultVal) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -155,6 +155,13 @@ public class MCFragment extends Fragment {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
view.findViewById(R.id.button_import_open5e).setOnClickListener(v -> {
|
||||
if (getActivity() instanceof MainActivity) {
|
||||
((MainActivity) getActivity()).importAllFromOpen5e();
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,14 @@ public class SnackbarHelper {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user