diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..603b140
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..681f41a
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..ac6b0ae
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a5f05cd
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..37a7509
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..d67657b
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,34 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+ defaultConfig {
+ applicationId 'ca.printf.dndb'
+ minSdkVersion 14
+ targetSdkVersion 19
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/ca/printf/dndb/ExampleInstrumentedTest.java b/app/src/androidTest/java/ca/printf/dndb/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..8adb5e3
--- /dev/null
+++ b/app/src/androidTest/java/ca/printf/dndb/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package ca.printf.dndb;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("ca.printf.dndb", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0810ac4
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..bd688a4
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/ca/printf/dndb/data/CommonIO.java b/app/src/main/java/ca/printf/dndb/data/CommonIO.java
new file mode 100644
index 0000000..56059a4
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/data/CommonIO.java
@@ -0,0 +1,190 @@
+package ca.printf.dndb.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+public class CommonIO {
+ private static final String MANIFEST_FILENAME = "Manifest.xml";
+ private static final String MANIFEST_FILE_TAG = "AssetFile";
+
+ public static String sanitizeString(String str) {
+ StringTokenizer tok = new StringTokenizer(str, "'");
+ String ret = "";
+ while(tok.hasMoreTokens())
+ ret += (tok.nextToken() + "''");
+ if(ret.isEmpty())
+ return ret;
+ return ret.substring(0, ret.length() - 2);
+ }
+
+ public static void execSQLFromFile(Context c, int res_id, SQLiteDatabase db) throws IOException {
+ InputStream is = c.getResources().openRawResource(res_id);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+ execSQLFromStream(reader, db);
+ reader.close();
+ is.close();
+ }
+
+ private static void execSQLFromStream(BufferedReader reader, SQLiteDatabase db) throws IOException {
+ while(reader.ready()) {
+ String stmt = getSQLStatement(reader);
+ if(!stmt.trim().isEmpty()) {
+ Log.d("execSQLFromFile", stmt);
+ db.execSQL(stmt);
+ }
+ }
+ }
+
+ private static void execSQLFromString(String str, SQLiteDatabase db) throws IOException {
+ StringTokenizer tok = new StringTokenizer(str, "\n", false);
+ String stmt = "";
+ while(tok.hasMoreTokens()) {
+ stmt += tok.nextToken();
+ stmt += "\n";
+ if(!stmt.trim().isEmpty() && stmt.charAt(stmt.length() - 2) == ';') {
+ Log.d("execSQLFromString", stmt);
+ db.execSQL(stmt.trim());
+ stmt = "";
+ }
+ }
+ }
+
+ private static String getSQLStatement(BufferedReader reader) throws IOException {
+ String stmt = "";
+ char prev = '\0';
+ while(reader.ready()) {
+ char next = (char)reader.read();
+ if(stmt.isEmpty() && next == '-') {
+ reader.readLine();
+ continue;
+ }
+ stmt += next;
+ if(next == '\n' && prev == ';')
+ break;
+ prev = next;
+ }
+ return stmt;
+ }
+
+ private static String getZipPackageManifest(ZipFile zf) throws IOException {
+ ZipEntry man = zf.getEntry(MANIFEST_FILENAME);
+ if(man == null)
+ return null;
+ return getZipFileContent(zf, man);
+ }
+
+ private static ArrayList getFileListFromManifest(String manifest) throws IOException {
+ ArrayList files = new ArrayList<>();
+ try {
+ XmlPullParser xml = XmlPullParserFactory.newInstance().newPullParser();
+ xml.setInput(new StringReader(manifest));
+ while(xml.next() != XmlPullParser.END_DOCUMENT) {
+ if(xml.getEventType() == XmlPullParser.START_TAG && xml.getName().equals(MANIFEST_FILE_TAG)){
+ if(xml.next() == XmlPullParser.TEXT) {
+ files.add(xml.getText());
+ Log.d("getFileListFromManifest", "Discovered file: " + files.get(files.size() - 1));
+ }
+ }
+ }
+ } catch (XmlPullParserException e) {
+ throw new IOException(e);
+ }
+ return files;
+ }
+
+ private static String getZipFileContent(ZipFile zf, ZipEntry ze) throws IOException {
+ ZipInputStream zis = new ZipInputStream(zf.getInputStream(ze));
+ String ret = _readZipStreamEntry(ze, zis);
+ zis.close();
+ return ret;
+ }
+
+ private static String _readZipStreamEntry(ZipEntry ze, ZipInputStream zis) throws IOException {
+ int size = (int)ze.getSize();
+ byte[] buf = new byte[size];
+ int bytes_read;
+ for(bytes_read = 0; bytes_read < size;) {
+ int bytes = zis.read(buf, bytes_read, size - bytes_read);
+ if(bytes == -1)
+ break;
+ bytes_read += bytes;
+ }
+ Log.d("getZipFileContent", bytes_read + " bytes read from " + ze.getName());
+ return new String(buf);
+ }
+
+ private static ArrayList getZipFilesContents(ZipFile zf, ArrayList filenames) throws IOException {
+ ArrayList contents = new ArrayList<>();
+ for(String file : filenames) {
+ ZipEntry entry = zf.getEntry(file);
+ if(entry == null) {
+ Log.w("getZipFilesContents", file + " not found in " + zf.getName());
+ continue;
+ }
+ String s = getZipFileContent(zf, entry);
+ if(s.isEmpty())
+ Log.w("getZipFilesContents", "No data read from " + file);
+ else
+ contents.add(s);
+ }
+ return contents;
+ }
+
+ public static void execSQLFromZipFile(ZipFile zf, SQLiteDatabase db) throws IOException {
+ String man = getZipPackageManifest(zf);
+ if(man == null || man.isEmpty())
+ throw new IOException("Could not read " + MANIFEST_FILENAME + " from zip file " + zf.getName());
+ ArrayList files = getFileListFromManifest(man);
+ Log.d("execSQLFromZipFile", "Discovered " + files.size() + " files");
+ if(files.isEmpty())
+ return;
+ files = getZipFilesContents(zf, files);
+ for(String s : files)
+ execSQLFromStream(new BufferedReader(new StringReader(s)), db);
+ }
+
+ public static void execSQLFromZipStream(InputStream zipfile, SQLiteDatabase db) throws IOException {
+ Map files = new HashMap<>();
+ ZipInputStream zis = new ZipInputStream(zipfile);
+ while(true) {
+ ZipEntry ze = zis.getNextEntry();
+ if (ze == null)
+ break;
+ String filename = ze.getName();
+ String contents = _readZipStreamEntry(ze, zis);
+ files.put(filename, contents);
+ zis.closeEntry();
+ }
+ String man = files.get(MANIFEST_FILENAME);
+ if(man == null || man.isEmpty()) {
+ Log.e("execSQLFromZipStream", MANIFEST_FILENAME + " not found in ZipInputStream");
+ return;
+ }
+ ArrayList file_list = getFileListFromManifest(man);
+ for(String s : file_list) {
+ String contents = files.get(s);
+ if(contents == null || contents.isEmpty()) {
+ Log.w("execSQLFromZipStream", "Could not find contents of " + s + " in ZipInputStream");
+ } else {
+ Log.d("execSQLFromZipStream", "Processing " + s + " (" + contents.length() + " chars)");
+ execSQLFromString(contents, db);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ca/printf/dndb/data/DndbSQLManager.java b/app/src/main/java/ca/printf/dndb/data/DndbSQLManager.java
new file mode 100644
index 0000000..eef36b6
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/data/DndbSQLManager.java
@@ -0,0 +1,70 @@
+package ca.printf.dndb.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipFile;
+import ca.printf.dndb.R;
+
+public class DndbSQLManager extends SQLiteOpenHelper {
+ private static final String DB_NAME = "dndb.sqlite";
+ private static final int DB_VER = 1;
+ public static final String TABLE_SPELL = "spell";
+ public static final String TABLE_SCHOOL = "school";
+ public static final String TABLE_SPELL_TARGET = "spell_target";
+ public static final String TABLE_TARGET = "target";
+ public static final String TABLE_SPELL_ABILITY = "spell_ability";
+ public static final String TABLE_ABILITY = "ability";
+ public static final String TABLE_SPELL_ATTACK_TYPE = "spell_attack_type";
+ public static final String TABLE_ATTACK_TYPE = "attack_type";
+ public static final String TABLE_SPELL_DAMAGE_TYPE = "spell_damage_type";
+ public static final String TABLE_DAMAGE_TYPE = "damage_type";
+ public static final String TABLE_SPELL_CONDITION = "spell_condition";
+ public static final String TABLE_CONDITION = "condition";
+ public static final String TABLE_SPELL_SOURCE = "spell_source";
+ public static final String TABLE_SOURCE = "source";
+ public static final String TABLE_SPELL_CLASS_LIST = "spell_class_list";
+ public static final String TABLE_CLASS_LIST = "class_list";
+ public static final String TABLE_SPELL_COMPONENT = "spell_component";
+ public static final String TABLE_COMPONENT = "component";
+ private Context ctx;
+
+ public DndbSQLManager(Context c) {
+ super(c, DB_NAME, null, DB_VER);
+ this.ctx = c;
+ }
+
+ public void onCreate(SQLiteDatabase db) {
+ try {
+ CommonIO.execSQLFromFile(ctx, R.raw.spells_ddl, db);
+ CommonIO.execSQLFromFile(ctx, R.raw.spells_init_dml, db);
+ } catch (IOException e) {
+ Log.e(this.getClass().getName(), "Error creating database ", e);
+ }
+ }
+
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ onCreate(db);
+ }
+
+ public boolean dbHasSpells(SQLiteDatabase db) {
+ Cursor res = db.rawQuery("SELECT rowid FROM " + TABLE_SPELL + ";", null);
+ boolean ret = res.getCount() > 0;
+ res.close();
+ return ret;
+ }
+
+ public void execZipPackage(SQLiteDatabase db, File zipfile) throws IOException {
+ ZipFile zf = new ZipFile(zipfile);
+ CommonIO.execSQLFromZipFile(zf, db);
+ }
+
+ public void execZipPackage(SQLiteDatabase db, InputStream zipfile) throws IOException {
+ CommonIO.execSQLFromZipStream(zipfile, db);
+ }
+}
diff --git a/app/src/main/java/ca/printf/dndb/entity/Spell.java b/app/src/main/java/ca/printf/dndb/entity/Spell.java
new file mode 100644
index 0000000..28b8f90
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/entity/Spell.java
@@ -0,0 +1,243 @@
+package ca.printf.dndb.entity;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import ca.printf.dndb.data.DndbSQLManager;
+
+public class Spell implements Serializable {
+ private static final long serialVersionUID = 1L;
+ // Columns in "spell" table
+ public static final String COL_ID = DndbSQLManager.TABLE_SPELL + ".rowid";
+ public static final String COL_NAME = DndbSQLManager.TABLE_SPELL + ".name";
+ public static final String COL_DESC = DndbSQLManager.TABLE_SPELL + ".description";
+ public static final String COL_HIGHER_DESC = DndbSQLManager.TABLE_SPELL + ".higher_level_description";
+ public static final String COL_LEVEL = DndbSQLManager.TABLE_SPELL + ".level";
+ public static final String COL_CONCENTRATION = DndbSQLManager.TABLE_SPELL + ".concentration";
+ public static final String COL_RITUAL = DndbSQLManager.TABLE_SPELL + ".ritual";
+ public static final String COL_RANGE = DndbSQLManager.TABLE_SPELL + ".range";
+ public static final String COL_DURATION = DndbSQLManager.TABLE_SPELL + ".duration";
+ public static final String COL_CAST_TIME = DndbSQLManager.TABLE_SPELL + ".casting_time";
+ public static final String COL_REACTION_DESC = DndbSQLManager.TABLE_SPELL + ".reaction_condition";
+ public static final String COL_MATERIALS = DndbSQLManager.TABLE_SPELL + ".materials";
+ public static final String COL_MATERIALS_COST = DndbSQLManager.TABLE_SPELL + ".materials_cost";
+ // Columns in "school" table
+ public static final String COL_SCHOOL = DndbSQLManager.TABLE_SCHOOL + ".name";
+ public static final String JOIN_SCHOOL = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_SCHOOL);
+ // Columns in "target" table
+ public static final String COL_TARGET = DndbSQLManager.TABLE_TARGET + ".type";
+ public static final String JOIN_TARGET = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_TARGET, DndbSQLManager.TABLE_SPELL_TARGET);
+ // Columns in "ability" table
+ public static final String COL_ABILITY_SHORTNAME = DndbSQLManager.TABLE_ABILITY + ".shortname";
+ public static final String COL_ABILITY_FULLNAME = DndbSQLManager.TABLE_ABILITY + ".name";
+ public static final String JOIN_ABILITY = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_ABILITY, DndbSQLManager.TABLE_SPELL_ABILITY);
+ // Columns in "attack_type" table
+ public static final String COL_ATK_TYPE = DndbSQLManager.TABLE_ATTACK_TYPE + ".type";
+ public static final String JOIN_ATK_TYPE = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_ATTACK_TYPE, DndbSQLManager.TABLE_SPELL_ATTACK_TYPE);
+ // Columns in "damage_type" table
+ public static final String COL_DMG_TYPE = DndbSQLManager.TABLE_DAMAGE_TYPE + ".type";
+ public static final String JOIN_DMG_TYPE = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_DAMAGE_TYPE, DndbSQLManager.TABLE_SPELL_DAMAGE_TYPE);
+ // Columns in "condition" table
+ public static final String COL_CONDITION = DndbSQLManager.TABLE_CONDITION + ".name";
+ public static final String JOIN_CONDITION = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_CONDITION, DndbSQLManager.TABLE_SPELL_CONDITION);
+ // Columns in "source" table
+ public static final String COL_SOURCE_SHORTNAME = DndbSQLManager.TABLE_SOURCE + ".shortname";
+ public static final String COL_SOURCE_FULLNAME = DndbSQLManager.TABLE_SOURCE + ".name";
+ public static final String JOIN_SOURCE = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_SOURCE, DndbSQLManager.TABLE_SPELL_SOURCE);
+ // Columns in "class_list" table
+ public static final String COL_CLASS = DndbSQLManager.TABLE_CLASS_LIST + ".class";
+ public static final String JOIN_CLASS = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_CLASS_LIST, DndbSQLManager.TABLE_SPELL_CLASS_LIST);
+ // Columns in "component" table
+ public static final String COL_COMPONENT_SYMBOL = DndbSQLManager.TABLE_COMPONENT + ".symbol";
+ public static final String COL_COMPONENT_NAME = DndbSQLManager.TABLE_COMPONENT + ".name";
+ public static final String JOIN_COMPONENT = JOIN_SPELL_TABLE(DndbSQLManager.TABLE_COMPONENT, DndbSQLManager.TABLE_SPELL_COMPONENT);
+ // Spell table columns for queries
+ public static final String[] QUERY_SPELL_COLS = {COL_ID, COL_NAME, COL_DESC,
+ COL_HIGHER_DESC, COL_CONCENTRATION, COL_RITUAL, COL_RANGE, COL_DURATION, COL_LEVEL,
+ COL_SCHOOL + " AS school_name", COL_CAST_TIME, COL_REACTION_DESC, COL_MATERIALS, COL_MATERIALS_COST};
+ public static final String[] QUERY_TARGET_COLS = {COL_TARGET};
+ public static final String[] QUERY_ABILITY_COLS = {COL_ABILITY_SHORTNAME, COL_ABILITY_FULLNAME};
+ public static final String[] QUERY_ATK_TYPE_COLS = {COL_ATK_TYPE};
+ public static final String[] QUERY_DMG_TYPE_COLS = {COL_DMG_TYPE};
+ public static final String[] QUERY_CONDITION_COLS = {COL_CONDITION};
+ public static final String[] QUERY_SOURCE_COLS = {COL_SOURCE_SHORTNAME, COL_SOURCE_FULLNAME};
+ public static final String[] QUERY_CLASS_COLS = {COL_CLASS};
+ public static final String[] QUERY_COMPONENT_COLS = {COL_COMPONENT_SYMBOL, COL_COMPONENT_NAME};
+ // Attribute options for filter spinners
+ public static final String QUERY_LEVEL_OPTIONS = QUERY_ATTR_OPTIONS(COL_LEVEL, DndbSQLManager.TABLE_SPELL);
+ public static final String QUERY_SCHOOL_OPTIONS = QUERY_ATTR_OPTIONS(COL_SCHOOL, DndbSQLManager.TABLE_SCHOOL);
+ public static final String QUERY_DURATION_OPTIONS = QUERY_ATTR_OPTIONS(COL_DURATION, DndbSQLManager.TABLE_SPELL);
+ public static final String QUERY_CASTTIME_OPTIONS = QUERY_ATTR_OPTIONS(COL_CAST_TIME, DndbSQLManager.TABLE_SPELL);
+ public static final String QUERY_TARGET_OPTIONS = QUERY_ATTR_OPTIONS(COL_TARGET, DndbSQLManager.TABLE_TARGET);
+ public static final String QUERY_ABILITY_OPTIONS = QUERY_ATTR_OPTIONS(COL_ABILITY_SHORTNAME, DndbSQLManager.TABLE_ABILITY);
+ public static final String QUERY_ATK_TYPE_OPTIONS = QUERY_ATTR_OPTIONS(COL_ATK_TYPE, DndbSQLManager.TABLE_ATTACK_TYPE);
+ public static final String QUERY_DMG_TYPE_OPTIONS = QUERY_ATTR_OPTIONS(COL_DMG_TYPE, DndbSQLManager.TABLE_DAMAGE_TYPE);
+ public static final String QUERY_CONDITION_OPTIONS = QUERY_ATTR_OPTIONS(COL_CONDITION, DndbSQLManager.TABLE_CONDITION);
+ public static final String QUERY_SOURCE_OPTIONS = QUERY_ATTR_OPTIONS(COL_SOURCE_SHORTNAME, DndbSQLManager.TABLE_SOURCE);
+ public static final String QUERY_CLASS_OPTIONS = QUERY_ATTR_OPTIONS(COL_CLASS, DndbSQLManager.TABLE_CLASS_LIST);
+ // Instance vars
+ private long id;
+ private String name;
+ private String desc;
+ private String higher_desc;
+ private int level;
+ private boolean concentration;
+ private boolean ritual;
+ private String range;
+ private ArrayList targets = new ArrayList<>();
+ private String duration;
+ private String cast_time;
+ private String reaction_desc;
+ private String school;
+ private boolean comp_v;
+ private boolean comp_s;
+ private boolean comp_m;
+ private String materials;
+ private int materials_cost;
+ private ArrayList ability_saves = new ArrayList<>();
+ private ArrayList atk_types = new ArrayList<>();
+ private ArrayList dmg_types = new ArrayList<>();
+ private ArrayList conditions = new ArrayList<>();
+ private ArrayList sources = new ArrayList<>();
+ private ArrayList classes = new ArrayList<>();
+
+ public Spell(long id) {this.id = id;}
+ public Spell() {this.id = -1;}
+
+ public long getId() {return id;}
+ public String getName() {return name;}
+ public String getDesc() {return desc;}
+ public String getHigherDesc() {return higher_desc;}
+ public int getLevel() {return level;}
+ public boolean isConcentration() {return concentration;}
+ public boolean isRitual() {return ritual;}
+ public String getRange() {return range;}
+ public ArrayList getTargets() {return targets;}
+ public String getDuration() {return duration;}
+ public String getCastTime() {return cast_time;}
+ public String getReactionDesc() {return reaction_desc;}
+ public String getSchool() {return school;}
+ public boolean isVerbal() {return comp_v;}
+ public boolean isSomatic() {return comp_s;}
+ public boolean isMaterial() {return comp_m;}
+ public String getMaterials() {return materials;}
+ public int getMaterialsCost() {return materials_cost;}
+ public ArrayList getAbilitySaves() {return ability_saves;}
+ public ArrayList getAtkTypes() {return atk_types;}
+ public ArrayList getDmgTypes() {return dmg_types;}
+ public ArrayList getConditions() {return conditions;}
+ public ArrayList getSources() {return sources;}
+ public ArrayList getClasses() {return classes;}
+
+ public void setId(long id) {this.id = id;}
+ public void setName(String name) {this.name = name;}
+ public void setDesc(String desc) {this.desc = desc;}
+ public void setHigherDesc(String higher_desc) {this.higher_desc = higher_desc;}
+ public void setLevel(int level) {this.level = level;}
+ public void setConcentration(boolean concentration) {this.concentration = concentration;}
+ public void setRitual(boolean ritual) {this.ritual = ritual;}
+ public void setRange(String range) {this.range = range;}
+ public void setTargets(ArrayList targets) {this.targets = targets;}
+ public void setDuration(String duration) {this.duration = duration;}
+ public void setCastTime(String cast_time) {this.cast_time = cast_time;}
+ public void setReactionDesc(String reaction_desc) {this.reaction_desc = reaction_desc;}
+ public void setSchool(String school) {this.school = school;}
+ public void setVerbal(boolean comp_v) {this.comp_v = comp_v;}
+ public void setSomatic(boolean comp_s) {this.comp_s = comp_s;}
+ public void setMaterial(boolean comp_m) {this.comp_m = comp_m;}
+ public void setMaterials(String materials) {this.materials = materials;}
+ public void setMaterialsCost(int materials_cost) {this.materials_cost = materials_cost;}
+ public void setAbilitySaves(ArrayList ability_saves) {this.ability_saves = ability_saves;}
+ public void setAtkTypes(ArrayList atk_types) {this.atk_types = atk_types;}
+ public void setDmgTypes(ArrayList dmg_types) {this.dmg_types = dmg_types;}
+ public void setConditions(ArrayList conditions) {this.conditions = conditions;}
+ public void setSources(ArrayList sources) {this.sources = sources;}
+ public void setClasses(ArrayList classes) {this.classes = classes;}
+
+ private static final String JOIN_SPELL_TABLE(final String TABLE) {
+ return "INNER JOIN " + TABLE + " ON " + TABLE + ".rowid = "
+ + DndbSQLManager.TABLE_SPELL + "." + TABLE;
+ }
+
+ private static final String JOIN_SPELL_TABLE(final String TABLE, final String JOIN_TABLE) {
+ return "INNER JOIN " + JOIN_TABLE + " ON " + DndbSQLManager.TABLE_SPELL + ".rowid = "
+ + JOIN_TABLE + "." + DndbSQLManager.TABLE_SPELL + "_id INNER JOIN " + TABLE + " ON "
+ + TABLE + ".rowid = " + JOIN_TABLE + "." + TABLE + "_id";
+ }
+
+ private static final String COLATE_COLS(final String[] COLS) {
+ String ret = "";
+ for(String s : COLS)
+ ret += (s + ",");
+ if(ret.isEmpty())
+ return ret;
+ return ret.substring(0, ret.length() - 1);
+ }
+
+ private static String createWhereClause(String spellname) {
+ if(spellname == null || spellname.isEmpty())
+ return " ";
+ spellname = ca.printf.dndb.data.CommonIO.sanitizeString(spellname);
+ return " WHERE " + COL_NAME + " LIKE '" + spellname + "'";
+ }
+
+ public static String querySpell() {
+ return querySpell(null);
+ }
+
+ public static String querySpell(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_SPELL_COLS) + " FROM " + DndbSQLManager.TABLE_SPELL
+ + " " + JOIN_SCHOOL + createWhereClause(spellname) + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String queryComponent(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_COMPONENT_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_COMPONENT + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String queryTarget(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_TARGET_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_TARGET + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String queryAbility(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_ABILITY_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_ABILITY + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String queryAttackType(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_ATK_TYPE_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_ATK_TYPE + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String queryDamageType(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_DMG_TYPE_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_DMG_TYPE + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String queryCondition(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_CONDITION_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_CONDITION + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String querySource(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_SOURCE_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_SOURCE + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ public static String queryClass(String spellname) {
+ return "SELECT " + COLATE_COLS(QUERY_CLASS_COLS) + " FROM "
+ + DndbSQLManager.TABLE_SPELL + " " + JOIN_CLASS + createWhereClause(spellname)
+ + " ORDER BY " + COL_NAME + ";";
+ }
+
+ private static final String QUERY_ATTR_OPTIONS(final String COL, final String TABLE) {
+ return "SELECT DISTINCT(" + COL + ") FROM " + TABLE + " ORDER BY " + COL + ";";
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ca/printf/dndb/list/SpellFilterAttributeSpinner.java b/app/src/main/java/ca/printf/dndb/list/SpellFilterAttributeSpinner.java
new file mode 100644
index 0000000..46d0147
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/list/SpellFilterAttributeSpinner.java
@@ -0,0 +1,65 @@
+package ca.printf.dndb.list;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+import java.util.ArrayList;
+
+public class SpellFilterAttributeSpinner implements SpinnerAdapter {
+ private ArrayList attributes;
+ private Context ctx;
+
+ public SpellFilterAttributeSpinner(ArrayList attributes, Context ctx) {
+ this.attributes = attributes;
+ this.ctx = ctx;
+ }
+ public int getCount() {
+ return attributes.size();
+ }
+
+ public Object getItem(int position) {
+ return attributes.get(position);
+ }
+
+ public long getItemId(int position) {
+ return -1;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return createListItemText(attributes.get(position), convertView);
+ }
+
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ public boolean isEmpty() {
+ return attributes.isEmpty();
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return getView(position, convertView, parent);
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {}
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {}
+
+ private TextView createListItemText(String txt, View old) {
+ if(!(old instanceof TextView))
+ old = new TextView(ctx);
+ ((TextView)old).setText(txt);
+ return (TextView)old;
+ }
+}
diff --git a/app/src/main/java/ca/printf/dndb/list/SpellListManager.java b/app/src/main/java/ca/printf/dndb/list/SpellListManager.java
new file mode 100644
index 0000000..c3201e9
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/list/SpellListManager.java
@@ -0,0 +1,50 @@
+package ca.printf.dndb.list;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import java.util.ArrayList;
+import ca.printf.dndb.R;
+import ca.printf.dndb.entity.Spell;
+
+public class SpellListManager extends BaseAdapter {
+ private ArrayList spells;
+ private Window parentActivity;
+
+ public SpellListManager(ArrayList spells, Window thisActivity) {
+ this.spells = spells;
+ this.parentActivity = thisActivity;
+ }
+
+ public void setSpells(ArrayList spells) {
+ this.spells = spells;
+ }
+
+ public int getCount() {return spells.size();}
+
+ public Object getItem(int pos) {return spells.get(pos);}
+
+ public long getItemId(int pos) {return ((Spell)getItem(pos)).getId();}
+
+ public View getView(int pos, View old, ViewGroup parent) {
+ Spell s = (Spell)getItem(pos);
+ View v = (old != null) ? old : parentActivity
+ .getLayoutInflater()
+ .inflate(R.layout.spells_listview_item, parent, false);
+ ((TextView)v.findViewById(R.id.spells_listview_item_spellname)).setText(s.getName());
+ String lvl = s.getLevel() > 0 ? Integer.toString(s.getLevel()) : "Cantrip";
+ ((TextView)v.findViewById(R.id.spells_listview_item_level)).setText(lvl);
+ ((TextView)v.findViewById(R.id.spells_listview_item_school)).setText(s.getSchool());
+ ((TextView)v.findViewById(R.id.spells_listview_item_casttime)).setText(s.getCastTime());
+ String comps = (s.isVerbal() ? "V" : "");
+ comps += (s.isSomatic() ? (comps.isEmpty() ? "S" : "/S") : "");
+ comps += (s.isMaterial() ? (comps.isEmpty() ? "M" : "/M") : "");
+ ((TextView)v.findViewById(R.id.spells_listview_item_component)).setText(comps);
+ ((ImageView)v.findViewById(R.id.spells_listview_item_concentration)).setVisibility(s.isConcentration() ? View.VISIBLE : View.GONE);
+ ((ImageView)v.findViewById(R.id.spells_listview_item_ritual)).setVisibility(s.isRitual() ? View.VISIBLE : View.GONE);
+ return v;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ca/printf/dndb/list/SpellSortComparator.java b/app/src/main/java/ca/printf/dndb/list/SpellSortComparator.java
new file mode 100644
index 0000000..fa2b6d3
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/list/SpellSortComparator.java
@@ -0,0 +1,38 @@
+package ca.printf.dndb.list;
+
+import java.util.Comparator;
+import ca.printf.dndb.entity.Spell;
+
+public class SpellSortComparator implements Comparator {
+ public static final String SORT_NAME = "Spell Name";
+ public static final String SORT_LEVEL = "Level";
+ public static final String SORT_SCHOOL = "School";
+ public static final String SORT_DURATION = "Duration";
+ public static final String SORT_CASTTIME = "Casting Time";
+ public static final String SORT_RANGE = "Range";
+ public static final String SORT_MATCOST = "Material Cost";
+ private String sortCondition;
+
+ public SpellSortComparator() {this(SORT_NAME);}
+ public SpellSortComparator(String sortCondition) {this.sortCondition = sortCondition;}
+ public void setSortCondition(String sortCondition) {this.sortCondition = sortCondition;}
+
+ public int compare(Spell s1, Spell s2) {
+ switch(sortCondition) {
+ case SORT_LEVEL :
+ return s1.getLevel() - s2.getLevel();
+ case SORT_SCHOOL :
+ return s1.getSchool().compareTo(s2.getSchool());
+ case SORT_DURATION :
+ return s1.getDuration().compareTo(s2.getDuration());
+ case SORT_CASTTIME :
+ return s1.getCastTime().compareTo(s2.getCastTime());
+ case SORT_RANGE :
+ return s1.getRange().compareTo(s2.getRange());
+ case SORT_MATCOST :
+ return s1.getMaterialsCost() - s2.getMaterialsCost();
+ default :
+ return s1.getName().compareTo(s2.getName());
+ }
+ }
+}
diff --git a/app/src/main/java/ca/printf/dndb/list/SpellSortSpinner.java b/app/src/main/java/ca/printf/dndb/list/SpellSortSpinner.java
new file mode 100644
index 0000000..54941ef
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/list/SpellSortSpinner.java
@@ -0,0 +1,39 @@
+package ca.printf.dndb.list;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+import java.util.ArrayList;
+
+public class SpellSortSpinner implements SpinnerAdapter {
+ private ArrayList sortlist;
+ private Context ctx;
+
+ public SpellSortSpinner(ArrayList sortlist, Context ctx) {
+ this.sortlist = sortlist;
+ this.ctx = ctx;
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {}
+ public void unregisterDataSetObserver(DataSetObserver observer) {}
+ public int getCount() {return sortlist.size();}
+ public Object getItem(int position) {return sortlist.get(position);}
+ public long getItemId(int position) {return -1;}
+ public boolean hasStableIds() {return true;}
+ public int getItemViewType(int position) {return 0;}
+ public int getViewTypeCount() {return 1;}
+ public boolean isEmpty() {return sortlist.isEmpty();}
+
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return getView(position, convertView, parent);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView tv = new TextView(ctx);
+ tv.setText(sortlist.get(position));
+ return tv;
+ }
+}
diff --git a/app/src/main/java/ca/printf/dndb/view/DefaultFragment.java b/app/src/main/java/ca/printf/dndb/view/DefaultFragment.java
new file mode 100644
index 0000000..c33a1f8
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/view/DefaultFragment.java
@@ -0,0 +1,16 @@
+package ca.printf.dndb.view;
+
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import ca.printf.dndb.R;
+
+public class DefaultFragment extends Fragment {
+ public DefaultFragment() {}
+ public void onCreate(Bundle b) {super.onCreate(b);}
+ public View onCreateView(LayoutInflater li, ViewGroup v, Bundle b) {
+ return li.inflate(R.layout.fragment_default, v, false);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ca/printf/dndb/view/ErrorFragment.java b/app/src/main/java/ca/printf/dndb/view/ErrorFragment.java
new file mode 100644
index 0000000..9632fe4
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/view/ErrorFragment.java
@@ -0,0 +1,51 @@
+package ca.printf.dndb.view;
+
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import ca.printf.dndb.R;
+
+public class ErrorFragment extends Fragment {
+ public static final String ERROR_HEADER = "header";
+ public static final String ERROR_MSG = "msg";
+
+ public void onCreate(Bundle b) {super.onCreate(b);}
+
+ public View onCreateView(LayoutInflater li, ViewGroup vg, Bundle b) {
+ View v = li.inflate(R.layout.error_screen, vg, false);
+ b = getArguments();
+ if(b == null)
+ return v;
+ String header = b.getString(ERROR_HEADER, null);
+ String msg = b.getString(ERROR_MSG, null);
+ if(header != null)
+ ((TextView)v.findViewById(R.id.error_header_txt)).setText(header);
+ if(msg != null)
+ ((TextView)v.findViewById(R.id.error_body_txt)).setText(msg);
+ return v;
+ }
+
+ public static void errorScreen(FragmentManager fragManager, String header, String msg) {
+ Bundle b = new Bundle();
+ b.putString(ErrorFragment.ERROR_HEADER, header);
+ b.putString(ErrorFragment.ERROR_MSG, msg);
+ Fragment errfrag = new ErrorFragment();
+ errfrag.setArguments(b);
+ fragManager.beginTransaction()
+ .addToBackStack(null)
+ .replace(R.id.content_frame, errfrag)
+ .commit();
+ }
+
+ public static void errorScreen(FragmentManager fragManager, String header, Throwable e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ errorScreen(fragManager, header, e.getMessage() + "\n" + sw.toString());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ca/printf/dndb/view/RootActivity.java b/app/src/main/java/ca/printf/dndb/view/RootActivity.java
new file mode 100644
index 0000000..1fc610c
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/view/RootActivity.java
@@ -0,0 +1,100 @@
+package ca.printf.dndb.view;
+
+import androidx.appcompat.app.ActionBarDrawerToggle;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.fragment.app.Fragment;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import ca.printf.dndb.R;
+import com.google.android.material.navigation.NavigationView;
+
+public class RootActivity extends AppCompatActivity
+ implements NavigationView.OnNavigationItemSelectedListener {
+ private static final String FRAG_DEFAULT = "FRAG_DEFAULT";
+ private static final String FRAG_SPELLS_LIST = "FRAG_SPELLS_LIST";
+ private Fragment content_frag;
+ private DrawerLayout drw;
+
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.root_activity);
+
+ Toolbar tb = findViewById(R.id.nav_toolbar);
+ setSupportActionBar(tb);
+
+ drw = findViewById(R.id.nav_root);
+ ActionBarDrawerToggle drw_tog =
+ new ActionBarDrawerToggle(this, drw, tb, R.string.app_name, R.string.app_name);
+ drw.addDrawerListener(drw_tog);
+ drw.openDrawer(GravityCompat.START);
+ drw_tog.syncState();
+
+ ((NavigationView)findViewById(R.id.nav_sidebar)).setNavigationItemSelectedListener(this);
+
+ content_frag = new DefaultFragment();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(R.id.content_frame, content_frag, FRAG_DEFAULT)
+ .commit();
+ }
+
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.nav_menu, menu);
+ menu.findItem(R.id.menu_spells).setVisible(false);
+ return true;
+ }
+
+ public boolean onNavigationItemSelected(MenuItem item) {
+ return menuAction(item);
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return menuAction(item);
+ }
+
+ private boolean menuAction(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_spells :
+ openFragSpells();
+ break;
+ case R.id.menu_about :
+ createAboutDialog().show();
+ break;
+ case R.id.menu_exit :
+ this.finish();
+ break;
+ default : break;
+ }
+ return true;
+ }
+
+ private AlertDialog createAboutDialog() {
+ AlertDialog.Builder about = new AlertDialog.Builder(this);
+ View v = getLayoutInflater().inflate(R.layout.about_dialog, null, false);
+ ((TextView)v.findViewById(R.id.about_text)).setMovementMethod(LinkMovementMethod.getInstance());
+ about.setView(v);
+ return about.create();
+ }
+
+ private void openFragSpells() {
+ ((FrameLayout)findViewById(R.id.content_frame)).removeAllViewsInLayout();
+ content_frag = new SpellsListFragment();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.content_frame, content_frag, FRAG_SPELLS_LIST)
+ .addToBackStack(null)
+ .commit();
+ if(drw.isDrawerOpen(GravityCompat.START))
+ drw.closeDrawer(GravityCompat.START, true);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ca/printf/dndb/view/SpellDetailsFragment.java b/app/src/main/java/ca/printf/dndb/view/SpellDetailsFragment.java
new file mode 100644
index 0000000..db99a26
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/view/SpellDetailsFragment.java
@@ -0,0 +1,102 @@
+package ca.printf.dndb.view;
+
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import java.util.ArrayList;
+import ca.printf.dndb.R;
+import ca.printf.dndb.entity.Spell;
+
+public class SpellDetailsFragment extends Fragment {
+ private Spell spell;
+
+ public SpellDetailsFragment(Spell spell) {
+ this.spell = spell;
+ }
+
+ public SpellDetailsFragment() {}
+
+ public void onCreate(Bundle b) {
+ super.onCreate(b);
+ }
+
+ public View onCreateView(LayoutInflater li, ViewGroup vg, Bundle b) {
+ View v = li.inflate(R.layout.fragment_spell_details, vg, false);
+ if(this.spell == null)
+ return v;
+ ((TextView)v.findViewById(R.id.spelldetail_spellname)).setText(spell.getName());
+ ((TextView)v.findViewById(R.id.spelldetail_description)).setText(convertNewlines(spell.getDesc()));
+ if(spell.getHigherDesc() != null && !spell.getHigherDesc().isEmpty()) {
+ TextView hd = v.findViewById(R.id.spelldetail_highlevel);
+ hd.setText(getString(R.string.general_spell_athigherlevels) + ". " + convertNewlines(spell.getHigherDesc()));
+ hd.setVisibility(View.VISIBLE);
+ }
+ String lvl = spell.getLevel() > 0 ? Integer.toString(spell.getLevel()) : "Cantrip";
+ ((TextView)v.findViewById(R.id.spelldetail_level)).setText(lvl);
+ if(spell.isConcentration())
+ ((ImageView)v.findViewById(R.id.spelldetail_concentration)).setVisibility(View.VISIBLE);
+ if(spell.isRitual())
+ ((ImageView)v.findViewById(R.id.spelldetail_ritual)).setVisibility(View.VISIBLE);
+ ((TextView)v.findViewById(R.id.spelldetail_range)).setText(spell.getRange());
+ ((TextView)v.findViewById(R.id.spelldetail_duration)).setText(spell.getDuration());
+ ((TextView)v.findViewById(R.id.spelldetail_casttime)).setText(spell.getCastTime());
+ if(spell.getReactionDesc() != null && !spell.getReactionDesc().isEmpty()) {
+ TextView rd = v.findViewById(R.id.spelldetail_reactiondesc);
+ rd.setText("(" + spell.getReactionDesc() + ")");
+ rd.setVisibility(View.VISIBLE);
+ }
+ ((TextView)v.findViewById(R.id.spelldetail_school)).setText(spell.getSchool());
+ String comps = (spell.isVerbal() ? "V" : "");
+ comps += (spell.isSomatic() ? (comps.isEmpty() ? "S" : "/S") : "");
+ comps += (spell.isMaterial() ? (comps.isEmpty() ? "M*" : "/M*") : "");
+ ((TextView)v.findViewById(R.id.spelldetail_component)).setText(comps);
+ if(spell.getMaterials() != null && !spell.getMaterials().isEmpty()) {
+ TextView mat = v.findViewById(R.id.spelldetail_materials);
+ mat.setText("* " + spell.getMaterials());
+ mat.setVisibility(View.VISIBLE);
+ }
+ ((TextView)v.findViewById(R.id.spelldetail_targets)).setText(colateStringList(spell.getTargets()));
+ displayStringList(spell.getAbilitySaves(), v, R.id.spelldetail_saves, R.id.spelldetail_saves_container);
+ displayStringList(spell.getAtkTypes(), v, R.id.spelldetail_attacks, R.id.spelldetail_attacks_container);
+ displayStringList(spell.getDmgTypes(), v, R.id.spelldetail_damages, R.id.spelldetail_damages_container);
+ displayStringList(spell.getConditions(), v, R.id.spelldetail_conditions, R.id.spelldetail_conditions_container);
+ ((TextView)v.findViewById(R.id.spelldetail_sources)).setText(colateStringList(spell.getSources()));
+ ((TextView)v.findViewById(R.id.spelldetail_classes)).setText(colateStringList(spell.getClasses()));
+ return v;
+ }
+
+ private String convertNewlines(String str) {
+ return str.replaceAll("\n", "\n\n");
+ }
+
+ private void displayStringList(ArrayList list, View parent, int textId, int containerId) {
+ String strlist = colateStringList(list);
+ if(strlist.trim().isEmpty())
+ return;
+ ((TextView)parent.findViewById(textId)).setText(strlist);
+ parent.findViewById(containerId).setVisibility(View.VISIBLE);
+ }
+
+ private String colateStringList(ArrayList list) {
+ String ret = "";
+ String delim = "";
+ for(String s : list) {
+ ret += (delim + s);
+ delim = ", ";
+ }
+ return ret;
+ }
+
+ public static void goToSpellDetails(FragmentManager fragManager, Spell spell) {
+ fragManager
+ .beginTransaction()
+ .addToBackStack(null)
+ .replace(R.id.content_frame, new SpellDetailsFragment(spell))
+ .commit();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ca/printf/dndb/view/SpellsListFragment.java b/app/src/main/java/ca/printf/dndb/view/SpellsListFragment.java
new file mode 100644
index 0000000..b5694d4
--- /dev/null
+++ b/app/src/main/java/ca/printf/dndb/view/SpellsListFragment.java
@@ -0,0 +1,422 @@
+package ca.printf.dndb.view;
+
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import ca.printf.dndb.R;
+import ca.printf.dndb.data.DndbSQLManager;
+import ca.printf.dndb.entity.Spell;
+import ca.printf.dndb.list.SpellFilterAttributeSpinner;
+import ca.printf.dndb.list.SpellListManager;
+import ca.printf.dndb.list.SpellSortComparator;
+import ca.printf.dndb.list.SpellSortSpinner;
+
+public class SpellsListFragment extends Fragment {
+ private ArrayList spells;
+ private DndbSQLManager dbman;
+ private SpellListManager adapter;
+ private AlertDialog filterPopup;
+ private AlertDialog sortPopup;
+
+ public SpellsListFragment() {
+ spells = new ArrayList<>();
+ }
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ dbman = new DndbSQLManager(getContext());
+ initSpells();
+ try {
+ readSpellsFromDB();
+ } catch (Exception e) {
+ ErrorFragment.errorScreen(getActivity().getSupportFragmentManager(),
+ "readSpellsFromDB: Error reading spells from database", e);
+ }
+ }
+
+ public View onCreateView(LayoutInflater li, ViewGroup v, Bundle b) {
+ adapter = new SpellListManager(spells, this.getActivity().getWindow());
+ View root = li.inflate(R.layout.fragment_spells_list, v, false);
+ ListView list = root.findViewById(R.id.spells_listview);
+ list.setAdapter(adapter);
+ list.setOnItemClickListener(spellSelection);
+ ((Button)root.findViewById(R.id.spells_btn_filter_spells)).setOnClickListener(filterButton);
+ ((Button)root.findViewById(R.id.spells_btn_sort_spells)).setOnClickListener(sortButton);
+ return root;
+ }
+
+ private AdapterView.OnItemClickListener spellSelection = new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView> parent, View v, int pos, long id) {
+ SpellDetailsFragment.goToSpellDetails(getActivity().getSupportFragmentManager(), spells.get(pos));
+ }
+ };
+
+ private View.OnClickListener filterButton = new View.OnClickListener() {
+ public void onClick(View btn) {
+ filterPopup = createFilterDialog().create();
+ filterPopup.show();
+ }
+ };
+
+ private View.OnClickListener sortButton = new View.OnClickListener() {
+ public void onClick(View v) {
+ sortPopup = createSortDialog().create();
+ sortPopup.show();
+ }
+ };
+
+ private AlertDialog.Builder createSortDialog() {
+ AlertDialog.Builder sortDialog = new AlertDialog.Builder(getContext());
+ View sortLayout = getLayoutInflater().inflate(R.layout.spell_sort_dialog, null, false);
+ Spinner sort = sortLayout.findViewById(R.id.spellsort_spinner);
+ sort.setAdapter(new SpellSortSpinner(createSortByList(), getContext()));
+ sortDialog.setView(sortLayout);
+ sortDialog.setNegativeButton(R.string.general_button_cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {dialog.dismiss();}
+ });
+ sortDialog.setPositiveButton(R.string.general_button_sort, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String sortitem = (String)((Spinner)sortPopup.findViewById(R.id.spellsort_spinner)).getSelectedItem();
+ sortSpellsBy(sortitem);
+ }
+ });
+ return sortDialog;
+ }
+
+ private ArrayList createSortByList() {
+ ArrayList ret = new ArrayList<>();
+ ret.add(SpellSortComparator.SORT_NAME);
+ ret.add(SpellSortComparator.SORT_LEVEL);
+ ret.add(SpellSortComparator.SORT_SCHOOL);
+ ret.add(SpellSortComparator.SORT_DURATION);
+ ret.add(SpellSortComparator.SORT_CASTTIME);
+ ret.add(SpellSortComparator.SORT_RANGE);
+ ret.add(SpellSortComparator.SORT_MATCOST);
+ return ret;
+ }
+
+ private void sortSpellsBy(String constraint) {
+ Collections.sort(spells, new SpellSortComparator(constraint));
+ adapter.setSpells(spells);
+ adapter.notifyDataSetChanged();
+ }
+
+ private AlertDialog.Builder createFilterDialog() {
+ AlertDialog.Builder filterDialog = new AlertDialog.Builder(getContext());
+ View filterLayout = getLayoutInflater().inflate(R.layout.spell_filter_dialog, null, false);
+
+ Spinner level = filterLayout.findViewById(R.id.spellfilter_level_spinner);
+ ArrayList levelOpts = spinnerAttributeOptions(R.string.label_spell_filter_level, Spell.QUERY_LEVEL_OPTIONS);
+ levelOpts.set(levelOpts.indexOf("0"), "Cantrip");
+ level.setAdapter(new SpellFilterAttributeSpinner(levelOpts, getContext()));
+
+ Spinner school = filterLayout.findViewById(R.id.spellfilter_school_spinner);
+ ArrayList schoolOpts = spinnerAttributeOptions(R.string.label_spell_filter_school, Spell.QUERY_SCHOOL_OPTIONS);
+ school.setAdapter(new SpellFilterAttributeSpinner(schoolOpts, getContext()));
+
+ Spinner duration = filterLayout.findViewById(R.id.spellfilter_duration_spinner);
+ ArrayList durationOpts = spinnerAttributeOptions(R.string.label_spell_filter_duration, Spell.QUERY_DURATION_OPTIONS);
+ duration.setAdapter(new SpellFilterAttributeSpinner(durationOpts, getContext()));
+
+ Spinner casttime = filterLayout.findViewById(R.id.spellfilter_casttime_spinner);
+ ArrayList casttimeOpts = spinnerAttributeOptions(R.string.label_spell_filter_casttime, Spell.QUERY_CASTTIME_OPTIONS);
+ casttime.setAdapter(new SpellFilterAttributeSpinner(casttimeOpts, getContext()));
+
+ Spinner target = filterLayout.findViewById(R.id.spellfilter_target_spinner);
+ ArrayList targetOpts = spinnerAttributeOptions(R.string.label_spell_filter_target, Spell.QUERY_TARGET_OPTIONS);
+ target.setAdapter(new SpellFilterAttributeSpinner(targetOpts, getContext()));
+
+ Spinner save = filterLayout.findViewById(R.id.spellfilter_ability_spinner);
+ ArrayList saveOpts = spinnerAttributeOptions(R.string.label_spell_filter_ability, Spell.QUERY_ABILITY_OPTIONS);
+ save.setAdapter(new SpellFilterAttributeSpinner(saveOpts, getContext()));
+
+ Spinner atktype = filterLayout.findViewById(R.id.spellfilter_atktype_spinner);
+ ArrayList atktypeOpts = spinnerAttributeOptions(R.string.label_spell_filter_atktype, Spell.QUERY_ATK_TYPE_OPTIONS);
+ atktype.setAdapter(new SpellFilterAttributeSpinner(atktypeOpts, getContext()));
+
+ Spinner dmgtype = filterLayout.findViewById(R.id.spellfilter_dmgtype_spinner);
+ ArrayList dmgtypeOpts = spinnerAttributeOptions(R.string.label_spell_filter_dmgtype, Spell.QUERY_DMG_TYPE_OPTIONS);
+ dmgtype.setAdapter(new SpellFilterAttributeSpinner(dmgtypeOpts, getContext()));
+
+ Spinner condition = filterLayout.findViewById(R.id.spellfilter_condition_spinner);
+ ArrayList conditionOpts = spinnerAttributeOptions(R.string.label_spell_filter_condition, Spell.QUERY_CONDITION_OPTIONS);
+ condition.setAdapter(new SpellFilterAttributeSpinner(conditionOpts, getContext()));
+
+ Spinner source = filterLayout.findViewById(R.id.spellfilter_source_spinner);
+ ArrayList sourceOpts = spinnerAttributeOptions(R.string.label_spell_filter_source, Spell.QUERY_SOURCE_OPTIONS);
+ source.setAdapter(new SpellFilterAttributeSpinner(sourceOpts, getContext()));
+
+ Spinner spellclass = filterLayout.findViewById(R.id.spellfilter_class_spinner);
+ ArrayList classOpts = spinnerAttributeOptions(R.string.label_spell_filter_class, Spell.QUERY_CLASS_OPTIONS);
+ spellclass.setAdapter(new SpellFilterAttributeSpinner(classOpts, getContext()));
+
+ filterDialog.setView(filterLayout);
+ filterDialog.setPositiveButton(R.string.general_button_search, applySearch);
+ filterDialog.setNegativeButton(R.string.general_button_cancel, cancelSearch);
+ return filterDialog;
+ }
+
+ private ArrayList spinnerAttributeOptions(int strId, String query) {
+ return spinnerAttributeOptions(getString(strId), query);
+ }
+
+ private ArrayList spinnerAttributeOptions(String defaultEntry, String query) {
+ ArrayList ret = new ArrayList<>();
+ ret.add("-- " + defaultEntry + " --");
+ SQLiteDatabase db = dbman.getReadableDatabase();
+ Cursor row = db.rawQuery(query, null);
+ while(row.moveToNext())
+ ret.add(row.getString(0));
+ row.close();
+ db.close();
+ return ret;
+ }
+
+ private DialogInterface.OnClickListener cancelSearch = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {dialog.dismiss();}
+ };
+
+ private DialogInterface.OnClickListener applySearch = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String name = ((EditText)filterPopup.findViewById(R.id.spellfilter_spellname)).getText().toString();
+ name = name.toLowerCase();
+ boolean check_desc = ((CheckBox)filterPopup.findViewById(R.id.spellfilter_description_checkbox)).isChecked();
+ boolean check_mats = ((CheckBox)filterPopup.findViewById(R.id.spellfilter_materialtxt_checkbox)).isChecked();
+ boolean concentration = ((CheckBox)filterPopup.findViewById(R.id.spellfilter_concentration_checkbox)).isChecked();
+ boolean ritual = ((CheckBox)filterPopup.findViewById(R.id.spellfilter_ritual_checkbox)).isChecked();
+ String tmpnum = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_level_spinner)).getSelectedItem();
+ int level = -1;
+ if(tmpnum != null && !tmpnum.trim().isEmpty() && !tmpnum.contains(getString(R.string.label_spell_filter_level)))
+ level = tmpnum.equals("Cantrip") ? 0 : Integer.parseInt(tmpnum);
+ String school = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_school_spinner)).getSelectedItem();
+ String duration = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_duration_spinner)).getSelectedItem();
+ String casttime = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_casttime_spinner)).getSelectedItem();
+ String target = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_target_spinner)).getSelectedItem();
+ String save = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_ability_spinner)).getSelectedItem();
+ String atktype = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_atktype_spinner)).getSelectedItem();
+ String dmgtype = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_dmgtype_spinner)).getSelectedItem();
+ String condition = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_condition_spinner)).getSelectedItem();
+ String source = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_source_spinner)).getSelectedItem();
+ String spellclass = (String)((Spinner)filterPopup.findViewById(R.id.spellfilter_class_spinner)).getSelectedItem();
+ boolean verbal = ((CheckBox)filterPopup.findViewById(R.id.spellfilter_verbal_checkbox)).isChecked();
+ boolean somatic = ((CheckBox)filterPopup.findViewById(R.id.spellfilter_somatic_checkbox)).isChecked();
+ boolean material = ((CheckBox)filterPopup.findViewById(R.id.spellfilter_material_checkbox)).isChecked();
+ tmpnum = ((EditText)filterPopup.findViewById(R.id.spellfilter_material_mincost)).getText().toString();
+ int mincost = -1;
+ if(tmpnum != null && !tmpnum.trim().isEmpty())
+ mincost = Integer.parseInt(tmpnum);
+ tmpnum = ((EditText)filterPopup.findViewById(R.id.spellfilter_material_maxcost)).getText().toString();
+ int maxcost = -1;
+ if(tmpnum != null && !tmpnum.trim().isEmpty())
+ maxcost= Integer.parseInt(tmpnum);
+ String range = ((EditText)filterPopup.findViewById(R.id.spellfilter_range)).getText().toString();
+ range = range.toLowerCase();
+ spells.clear();
+ readSpellsFromDB();
+ ArrayList spellsFiltered = new ArrayList<>();
+ for(Spell s : spells) {
+ if(!name.isEmpty()) {
+ if(!s.getName().toLowerCase().contains(name)
+ && !(check_desc && s.getDesc().toLowerCase().contains(name))
+ && !(check_mats && s.getMaterials() != null && s.getMaterials().toLowerCase().contains(name)))
+ continue;
+ }
+ if(concentration && !s.isConcentration())
+ continue;
+ if(ritual && !s.isRitual())
+ continue;
+ if(level > -1 && s.getLevel() != level)
+ continue;
+ if(!school.contains(getString(R.string.label_spell_filter_school)) && !school.equals(s.getSchool()))
+ continue;
+ if(!duration.contains(getString(R.string.label_spell_filter_duration)) && !duration.equals(s.getDuration()))
+ continue;
+ if(!casttime.contains(getString(R.string.label_spell_filter_casttime)) && !casttime.equals(s.getCastTime()))
+ continue;
+ if(!target.contains(getString(R.string.label_spell_filter_target))) {
+ boolean found = false;
+ for(String t : s.getTargets())
+ found = target.equals(t);
+ if(!found)
+ continue;
+ }
+ if(!save.contains(getString(R.string.label_spell_filter_ability))) {
+ boolean found = false;
+ for(String t : s.getAbilitySaves())
+ found = save.equals(t);
+ if(!found)
+ continue;
+ }
+ if(!atktype.contains(getString(R.string.label_spell_filter_atktype))) {
+ boolean found = false;
+ for(String t : s.getAtkTypes())
+ found = atktype.equals(t);
+ if(!found)
+ continue;
+ }
+ if(!dmgtype.contains(getString(R.string.label_spell_filter_dmgtype))) {
+ boolean found = false;
+ for(String t : s.getDmgTypes())
+ found = dmgtype.equals(t);
+ if(!found)
+ continue;
+ }
+ if(!condition.contains(getString(R.string.label_spell_filter_condition))) {
+ boolean found = false;
+ for(String t : s.getConditions())
+ found = condition.equals(t);
+ if(!found)
+ continue;
+ }
+ if(!source.contains(getString(R.string.label_spell_filter_source))) {
+ boolean found = false;
+ for(String t : s.getSources())
+ found = source.equals(t);
+ if(!found)
+ continue;
+ }
+ if(!spellclass.contains(getString(R.string.label_spell_filter_class))) {
+ boolean found = false;
+ for(String t : s.getClasses())
+ found = spellclass.equals(t);
+ if(!found)
+ continue;
+ }
+ if(verbal && !s.isVerbal())
+ continue;
+ if(somatic && !s.isSomatic())
+ continue;
+ if(material && !s.isMaterial())
+ continue;
+ if(mincost > -1 && (!s.isMaterial() || s.getMaterialsCost() < mincost))
+ continue;
+ if(maxcost > -1 && (!s.isMaterial() || s.getMaterialsCost() > maxcost))
+ continue;
+ if(!range.isEmpty() && !s.getRange().toLowerCase().contains(range))
+ continue;
+ spellsFiltered.add(s);
+ }
+ Log.d("DEBUG", "FILTERED SEARCH");
+ spells = spellsFiltered;
+ adapter.setSpells(spells);
+ adapter.notifyDataSetChanged();
+ }
+ };
+
+ private void initSpells() {
+ SQLiteDatabase db = dbman.getReadableDatabase();
+ if(dbman.dbHasSpells(db)) {
+ db.close();
+ return;
+ }
+ db.close();
+ insertDefaultSpells();
+ }
+
+ private void insertDefaultSpells() {
+ SQLiteDatabase db = dbman.getWritableDatabase();
+ InputStream zip = getResources().openRawResource(R.raw.srd);
+ try {
+ dbman.execZipPackage(db, zip);
+ } catch (IOException e) {
+ db.close();
+ Log.e("initSpells", "Error processing stream R.raw.srd", e);
+ ErrorFragment.errorScreen(getActivity().getSupportFragmentManager(),
+ "initSpells: Error processing stream R.raw.srd", e);
+ }
+ db.close();
+ }
+
+ private void readSpellsFromDB() {
+ SQLiteDatabase db = dbman.getReadableDatabase();
+ Cursor row = db.rawQuery(Spell.querySpell(), null);
+ Log.d("readSpellsFromDB", row.getCount() + " spells loaded");
+ while(row.moveToNext()) {
+ Spell s = new Spell(row.getLong(row.getColumnIndex(stripTableFromCol(Spell.COL_ID))));
+ s.setName(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_NAME))));
+ s.setLevel(row.getInt(row.getColumnIndex(stripTableFromCol(Spell.COL_LEVEL))));
+ s.setSchool(row.getString(row.getColumnIndex("school_" + stripTableFromCol(Spell.COL_SCHOOL))));
+ s.setCastTime(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_CAST_TIME))));
+ s.setConcentration(row.getInt(row.getColumnIndex(stripTableFromCol(Spell.COL_CONCENTRATION))) != 0);
+ s.setRitual(row.getInt(row.getColumnIndex(stripTableFromCol(Spell.COL_RITUAL))) != 0);
+ s.setDesc(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_DESC))));
+ s.setHigherDesc(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_HIGHER_DESC))));
+ s.setDuration(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_DURATION))));
+ s.setMaterials(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_MATERIALS))));
+ s.setMaterialsCost(row.getInt(row.getColumnIndex(stripTableFromCol(Spell.COL_MATERIALS_COST))));
+ s.setRange(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_RANGE))));
+ s.setReactionDesc(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_REACTION_DESC))));
+ spells.add(s);
+ }
+ row.close();
+ for(Spell s : spells)
+ setSpellMultivalues(s, db);
+ db.close();
+ }
+
+ private void setSpellMultivalues(Spell s, SQLiteDatabase db) {
+ Cursor row = db.rawQuery(Spell.queryComponent(s.getName()), null);
+ while(row.moveToNext()) {
+ String comp = row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_COMPONENT_SYMBOL)));
+ if(comp == null || comp.isEmpty())
+ continue;
+ if ("V".equals(comp)) {
+ s.setVerbal(true);
+ } else if ("S".equals(comp)) {
+ s.setSomatic(true);
+ } else if ("M".equals(comp)) {
+ s.setMaterial(true);
+ }
+ }
+ row.close();
+ row = db.rawQuery(Spell.queryTarget(s.getName()), null);
+ while(row.moveToNext())
+ s.getTargets().add(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_TARGET))));
+ row.close();
+ row = db.rawQuery(Spell.queryAbility(s.getName()), null);
+ while(row.moveToNext())
+ s.getAbilitySaves().add(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_ABILITY_SHORTNAME))));
+ row.close();
+ row = db.rawQuery(Spell.queryAttackType(s.getName()), null);
+ while(row.moveToNext())
+ s.getAtkTypes().add(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_ATK_TYPE))));
+ row.close();
+ row = db.rawQuery(Spell.queryDamageType(s.getName()), null);
+ while(row.moveToNext())
+ s.getDmgTypes().add(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_DMG_TYPE))));
+ row.close();
+ row = db.rawQuery(Spell.queryCondition(s.getName()), null);
+ while(row.moveToNext())
+ s.getConditions().add(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_CONDITION))));
+ row.close();
+ row = db.rawQuery(Spell.querySource(s.getName()), null);
+ while(row.moveToNext())
+ s.getSources().add(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_SOURCE_SHORTNAME))));
+ row.close();
+ row = db.rawQuery(Spell.queryClass(s.getName()), null);
+ while(row.moveToNext())
+ s.getClasses().add(row.getString(row.getColumnIndex(stripTableFromCol(Spell.COL_CLASS))));
+ row.close();
+ }
+
+ private String stripTableFromCol(String col) {
+ return col.substring(col.lastIndexOf('.') + 1);
+ }
+}
diff --git a/app/src/main/res/drawable/app_icon_foreground.png b/app/src/main/res/drawable/app_icon_foreground.png
new file mode 100644
index 0000000..160eac2
Binary files /dev/null and b/app/src/main/res/drawable/app_icon_foreground.png differ
diff --git a/app/src/main/res/drawable/concentration.png b/app/src/main/res/drawable/concentration.png
new file mode 100644
index 0000000..5979fed
Binary files /dev/null and b/app/src/main/res/drawable/concentration.png differ
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..a29cc45
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background_custom.xml b/app/src/main/res/drawable/ic_launcher_background_custom.xml
new file mode 100644
index 0000000..e99b90a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background_custom.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/left_spinner_arrow.xml b/app/src/main/res/drawable/left_spinner_arrow.xml
new file mode 100644
index 0000000..347fe0b
--- /dev/null
+++ b/app/src/main/res/drawable/left_spinner_arrow.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png
new file mode 100644
index 0000000..375862f
Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ
diff --git a/app/src/main/res/drawable/ritual.png b/app/src/main/res/drawable/ritual.png
new file mode 100644
index 0000000..6dcb613
Binary files /dev/null and b/app/src/main/res/drawable/ritual.png differ
diff --git a/app/src/main/res/layout/about_dialog.xml b/app/src/main/res/layout/about_dialog.xml
new file mode 100644
index 0000000..521cb59
--- /dev/null
+++ b/app/src/main/res/layout/about_dialog.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/error_screen.xml b/app/src/main/res/layout/error_screen.xml
new file mode 100644
index 0000000..39971e8
--- /dev/null
+++ b/app/src/main/res/layout/error_screen.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_default.xml b/app/src/main/res/layout/fragment_default.xml
new file mode 100644
index 0000000..3390f6c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_default.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_spell_details.xml b/app/src/main/res/layout/fragment_spell_details.xml
new file mode 100644
index 0000000..87bde96
--- /dev/null
+++ b/app/src/main/res/layout/fragment_spell_details.xml
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_spells_list.xml b/app/src/main/res/layout/fragment_spells_list.xml
new file mode 100644
index 0000000..f79bb1c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_spells_list.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/root_activity.xml b/app/src/main/res/layout/root_activity.xml
new file mode 100644
index 0000000..29e94df
--- /dev/null
+++ b/app/src/main/res/layout/root_activity.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/sidebar_header.xml b/app/src/main/res/layout/sidebar_header.xml
new file mode 100644
index 0000000..8eeddca
--- /dev/null
+++ b/app/src/main/res/layout/sidebar_header.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/spell_filter_dialog.xml b/app/src/main/res/layout/spell_filter_dialog.xml
new file mode 100644
index 0000000..d86ff96
--- /dev/null
+++ b/app/src/main/res/layout/spell_filter_dialog.xml
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/spell_sort_dialog.xml b/app/src/main/res/layout/spell_sort_dialog.xml
new file mode 100644
index 0000000..af6d9cc
--- /dev/null
+++ b/app/src/main/res/layout/spell_sort_dialog.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/spells_listview_item.xml b/app/src/main/res/layout/spells_listview_item.xml
new file mode 100644
index 0000000..141dc60
--- /dev/null
+++ b/app/src/main/res/layout/spells_listview_item.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/nav_menu.xml b/app/src/main/res/menu/nav_menu.xml
new file mode 100644
index 0000000..2733f41
--- /dev/null
+++ b/app/src/main/res/menu/nav_menu.xml
@@ -0,0 +1,32 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..948bd24
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..949f81b
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..de28de2
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..cc30bf6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..c787623
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b27a872
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..a08cdc9
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..16b6348
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8c08373
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..c7631df
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8d5ada3
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b199163
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2d11d18
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2124505
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..708ecd8
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/raw/spells_ddl.sql b/app/src/main/res/raw/spells_ddl.sql
new file mode 100644
index 0000000..2054423
--- /dev/null
+++ b/app/src/main/res/raw/spells_ddl.sql
@@ -0,0 +1,126 @@
+-- Example query: get all spells that have a "V" component
+-- SELECT spell.name, spell.description
+-- FROM spell
+-- INNER JOIN spell_component ON spell.rowid = spell_component.spell_id
+-- WHERE spell_component.component_id = (SELECT rowid FROM component WHERE symbol LIKE "V");
+
+DROP TABLE IF EXISTS spell;
+CREATE TABLE spell (
+ concentration INTEGER NOT NULL,
+ description TEXT NOT NULL,
+ higher_level_description TEXT,
+ duration TEXT,
+ casting_time TEXT,
+ reaction_condition TEXT,
+ name TEXT NOT NULL UNIQUE,
+ ritual INTEGER NOT NULL,
+ school INTEGER NOT NULL,
+ level INTEGER NOT NULL,
+ range TEXT,
+ materials TEXT,
+ materials_cost INTEGER
+);
+
+DROP TABLE IF EXISTS school;
+CREATE TABLE school (
+ name TEXT NOT NULL UNIQUE
+);
+
+DROP TABLE IF EXISTS spell_target;
+CREATE TABLE spell_target (
+ spell_id INTEGER NOT NULL,
+ target_id INTEGER NOT NULL,
+ UNIQUE(spell_id,target_id)
+);
+
+DROP TABLE IF EXISTS target;
+CREATE TABLE target (
+ type TEXT NOT NULL UNIQUE
+);
+
+DROP TABLE IF EXISTS spell_ability;
+CREATE TABLE spell_ability (
+ spell_id INTEGER NOT NULL,
+ ability_id INTEGER NOT NULL,
+ UNIQUE(spell_id,ability_id)
+);
+
+DROP TABLE IF EXISTS ability;
+CREATE TABLE ability (
+ shortname TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL
+);
+
+DROP TABLE IF EXISTS spell_attack_type;
+CREATE TABLE spell_attack_type (
+ spell_id INTEGER NOT NULL,
+ attack_type_id INTEGER NOT NULL,
+ UNIQUE(spell_id,attack_type_id)
+);
+
+DROP TABLE IF EXISTS attack_type;
+CREATE TABLE attack_type (
+ type TEXT NOT NULL UNIQUE
+);
+
+DROP TABLE IF EXISTS spell_damage_type;
+CREATE TABLE spell_damage_type (
+ spell_id INTEGER NOT NULL,
+ damage_type_id INTEGER NOT NULL,
+ UNIQUE(spell_id,damage_type_id)
+);
+
+DROP TABLE IF EXISTS damage_type;
+CREATE TABLE damage_type (
+ type TEXT NOT NULL UNIQUE
+);
+
+DROP TABLE IF EXISTS spell_condition;
+CREATE TABLE spell_condition (
+ spell_id INTEGER NOT NULL,
+ condition_id INTEGER NOT NULL,
+ UNIQUE(spell_id,condition_id)
+);
+
+DROP TABLE IF EXISTS condition;
+CREATE TABLE condition (
+ name TEXT NOT NULL UNIQUE
+);
+
+DROP TABLE IF EXISTS spell_source;
+CREATE TABLE spell_source (
+ spell_id INTEGER NOT NULL,
+ source_id INTEGER NOT NULL,
+ UNIQUE(spell_id,source_id)
+);
+
+DROP TABLE IF EXISTS source;
+CREATE TABLE source (
+ shortname TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL
+);
+
+DROP TABLE IF EXISTS spell_class_list;
+CREATE TABLE spell_class_list (
+ spell_id INTEGER NOT NULL,
+ class_list_id INTEGER NOT NULL,
+ UNIQUE(spell_id,class_list_id)
+);
+
+DROP TABLE IF EXISTS class_list;
+CREATE TABLE class_list (
+ class TEXT NOT NULL UNIQUE
+);
+
+DROP TABLE IF EXISTS spell_component;
+CREATE TABLE spell_component (
+ spell_id INTEGER NOT NULL,
+ component_id INTEGER NOT NULL,
+ UNIQUE(spell_id,component_id)
+);
+
+DROP TABLE IF EXISTS component;
+CREATE TABLE component (
+ symbol TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL
+);
diff --git a/app/src/main/res/raw/spells_init_dml.sql b/app/src/main/res/raw/spells_init_dml.sql
new file mode 100644
index 0000000..b25ec40
--- /dev/null
+++ b/app/src/main/res/raw/spells_init_dml.sql
@@ -0,0 +1,104 @@
+BEGIN TRANSACTION;
+
+DELETE FROM spell;
+DELETE FROM school;
+DELETE FROM spell_target;
+DELETE FROM target;
+DELETE FROM spell_ability;
+DELETE FROM ability;
+DELETE FROM spell_attack_type;
+DELETE FROM attack_type;
+DELETE FROM spell_damage_type;
+DELETE FROM damage_type;
+DELETE FROM spell_condition;
+DELETE FROM condition;
+DELETE FROM spell_source;
+DELETE FROM source;
+DELETE FROM spell_class_list;
+DELETE FROM class_list;
+DELETE FROM spell_component;
+DELETE FROM component;
+
+INSERT INTO school (name) VALUES
+ ('Abjuration'),
+ ('Conjuration'),
+ ('Divination'),
+ ('Enchantment'),
+ ('Evocation'),
+ ('Illusion'),
+ ('Necromancy'),
+ ('Transmutation');
+
+INSERT INTO target (type) VALUES
+ ('Self'),
+ ('Creature'),
+ ('Object'),
+ ('Point in space'),
+ ('Corpse');
+
+INSERT INTO ability (shortname,name) VALUES
+ ('STR','Strength'),
+ ('DEX','Dexterity'),
+ ('CON','Constitution'),
+ ('INT','Intelligence'),
+ ('WIS','Wisdom'),
+ ('CHA','Charisma');
+
+INSERT INTO attack_type (type) VALUES
+ ('Melee Weapon'),
+ ('Melee Spell'),
+ ('Ranged Weapon'),
+ ('Ranged Spell');
+
+INSERT INTO damage_type (type) VALUES
+ ('Bludgeoning'),
+ ('Piercing'),
+ ('Slashing'),
+ ('Acid'),
+ ('Cold'),
+ ('Fire'),
+ ('Force'),
+ ('Lightning'),
+ ('Necrotic'),
+ ('Poison'),
+ ('Psychic'),
+ ('Radiant'),
+ ('Thunder'),
+ ('Healing'),
+ ('Raw');
+
+INSERT INTO condition (name) VALUES
+ ('Blinded'),
+ ('Charmed'),
+ ('Confused'),
+ ('Deafened'),
+ ('Exhaustion'),
+ ('Frightened'),
+ ('Grappled'),
+ ('Incapacitated'),
+ ('Invisible'),
+ ('Paralyzed'),
+ ('Petrified'),
+ ('Poisoned'),
+ ('Prone'),
+ ('Restrained'),
+ ('Stunned'),
+ ('Unconscious');
+
+INSERT INTO class_list (class) VALUES
+ ('Bard'),
+ ('Cleric'),
+ ('Druid'),
+ ('Paladin'),
+ ('Ranger'),
+ ('Sorcerer'),
+ ('Warlock'),
+ ('Wizard'),
+ ('Artificer');
+
+INSERT INTO component (symbol,name) VALUES
+ ('V','Verbal'),
+ ('S','Somatic'),
+ ('M','Material');
+
+COMMIT;
diff --git a/app/src/main/res/raw/srd.zip b/app/src/main/res/raw/srd.zip
new file mode 100644
index 0000000..61fb913
Binary files /dev/null and b/app/src/main/res/raw/srd.zip differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..5b728a5
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #BBB
+ #222
+ #D22
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..beab31f
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #000000
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5fc5077
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,80 @@
+
+ ]>
+
+
+ &appname;
+ Settings
+ About
+ Exit
+ Spells
+ To get started, select a category
+ Filter Spells
+ Lvl :
+ School :
+ Casting Time :
+ Components :
+ Duration :
+ Range/Area :
+ Target(s) :
+ Attack Type(s) :
+ Damage Type(s) :
+ Condition(s) :
+ Saving Throw(s) :
+ Available for :
+ Spell Source(s) :
+ SPELLNAME
+ LEVEL
+ SCHOOL
+ CASTTIME
+ SPELLDESCRIPTION
+ SPELLHIGHERLVLDESC
+ COMPS
+ ERRORHEADER
+ ERRORBODY
+ DURATION
+ RANGE
+ TARGETS
+ None
+ None
+ None
+ None
+ REACTIONDESC
+ MATERIALS
+ CLASSES
+ SOURCES
+ At higher levels
+ Search
+ Cancel
+ Search Spells
+ Hunter\'s Mark
+ Also search spell description
+ Also search spell materials
+ Level
+ School
+ Duration
+ Casting Time
+ Concentration
+ Ritual
+ Verbal
+ Somatic
+ Material
+ Minimum Material Cost (in gp)
+ Maximum Material Cost (in gp)
+ Target
+ Saving Throw
+ Attack Type
+ Damage Type
+ Condition
+ Source Book
+ Spell Class
+ Range
+ Sort Spells
+ Sort by :
+ Sort
+ Sort Spells
+ &appname;, created by David Seguin\n\n
+ App Version : &appversion;\n
+ Source Code : GitHub\n
+ License : MIT
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..4ca5641
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/ca/printf/dndb/ExampleUnitTest.java b/app/src/test/java/ca/printf/dndb/ExampleUnitTest.java
new file mode 100644
index 0000000..9722e02
--- /dev/null
+++ b/app/src/test/java/ca/printf/dndb/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package ca.printf.dndb;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..343345b
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:4.0.0-beta05"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..c52ac9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3d5d0c1
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Aug 26 13:29:35 EDT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..c5ae5ad
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name = "DNDB"
\ No newline at end of file