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 @@ + + + +