diff --git a/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/5.json b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/5.json new file mode 100644 index 0000000..2c368ac --- /dev/null +++ b/Android/app/schemas/com.majinnaibu.monstercards.AppDatabase/5.json @@ -0,0 +1,609 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "6d497d0f0530c0f46bed1d57492e940e", + "entities": [ + { + "tableName": "monsters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `size` TEXT NOT NULL DEFAULT '', `type` TEXT NOT NULL DEFAULT '', `subtype` TEXT NOT NULL DEFAULT '', `alignment` TEXT NOT NULL DEFAULT '', `strength_score` INTEGER NOT NULL DEFAULT 10, `strength_saving_throw_advantage` TEXT DEFAULT 'none', `strength_saving_throw_proficiency` TEXT DEFAULT 'none', `dexterity_score` INTEGER NOT NULL DEFAULT 10, `dexterity_saving_throw_advantage` TEXT DEFAULT 'none', `dexterity_saving_throw_proficiency` TEXT DEFAULT 'none', `constitution_score` INTEGER NOT NULL DEFAULT 10, `constitution_saving_throw_advantage` TEXT DEFAULT 'none', `constitution_saving_throw_proficiency` TEXT DEFAULT 'none', `intelligence_score` INTEGER NOT NULL DEFAULT 10, `intelligence_saving_throw_advantage` TEXT DEFAULT 'none', `intelligence_saving_throw_proficiency` TEXT DEFAULT 'none', `wisdom_score` INTEGER NOT NULL DEFAULT 10, `wisdom_saving_throw_advantage` TEXT DEFAULT 'none', `wisdom_saving_throw_proficiency` TEXT DEFAULT 'none', `charisma_score` INTEGER NOT NULL DEFAULT 10, `charisma_saving_throw_advantage` TEXT DEFAULT 'none', `charisma_saving_throw_proficiency` TEXT DEFAULT 'none', `armor_type` TEXT DEFAULT 'none', `shield_bonus` INTEGER NOT NULL DEFAULT 0, `natural_armor_bonus` INTEGER NOT NULL DEFAULT 0, `other_armor_description` TEXT DEFAULT '', `hit_dice` INTEGER NOT NULL DEFAULT 1, `has_custom_hit_points` INTEGER NOT NULL, `custom_hit_points_description` TEXT DEFAULT '', `walk_speed` INTEGER NOT NULL DEFAULT 0, `burrow_speed` INTEGER NOT NULL DEFAULT 0, `climb_speed` INTEGER NOT NULL DEFAULT 0, `fly_speed` INTEGER NOT NULL DEFAULT 0, `can_hover` INTEGER NOT NULL DEFAULT false, `swim_speed` INTEGER NOT NULL DEFAULT 0, `has_custom_speed` INTEGER NOT NULL DEFAULT false, `custom_speed_description` TEXT, `challenge_rating` TEXT DEFAULT '1', `custom_challenge_rating_description` TEXT DEFAULT '', `custom_proficiency_bonus` INTEGER NOT NULL DEFAULT 0, `telepathy_range` INTEGER NOT NULL DEFAULT 0, `understands_but_description` TEXT DEFAULT '', `senses` TEXT DEFAULT '[]', `skills` TEXT DEFAULT '[]', `damage_immunities` TEXT DEFAULT '[]', `damage_resistances` TEXT DEFAULT '[]', `damage_vulnerabilities` TEXT DEFAULT '[]', `condition_immunities` TEXT DEFAULT '[]', `languages` TEXT DEFAULT '[]', `abilities` TEXT DEFAULT '[]', `actions` TEXT DEFAULT '[]', `reactions` TEXT DEFAULT '[]', `lair_actions` TEXT DEFAULT '[]', `legendary_actions` TEXT DEFAULT '[]', `regional_actions` TEXT DEFAULT '[]', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "strengthScore", + "columnName": "strength_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "strengthSavingThrowAdvantage", + "columnName": "strength_saving_throw_advantage", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "strengthSavingThrowProficiency", + "columnName": "strength_saving_throw_proficiency", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "dexterityScore", + "columnName": "dexterity_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "dexteritySavingThrowAdvantage", + "columnName": "dexterity_saving_throw_advantage", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "dexteritySavingThrowProficiency", + "columnName": "dexterity_saving_throw_proficiency", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionScore", + "columnName": "constitution_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "constitutionSavingThrowAdvantage", + "columnName": "constitution_saving_throw_advantage", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "constitutionSavingThrowProficiency", + "columnName": "constitution_saving_throw_proficiency", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceScore", + "columnName": "intelligence_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "intelligenceSavingThrowAdvantage", + "columnName": "intelligence_saving_throw_advantage", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "intelligenceSavingThrowProficiency", + "columnName": "intelligence_saving_throw_proficiency", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomScore", + "columnName": "wisdom_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "wisdomSavingThrowAdvantage", + "columnName": "wisdom_saving_throw_advantage", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "wisdomSavingThrowProficiency", + "columnName": "wisdom_saving_throw_proficiency", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaScore", + "columnName": "charisma_score", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + }, + { + "fieldPath": "charismaSavingThrowAdvantage", + "columnName": "charisma_saving_throw_advantage", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "charismaSavingThrowProficiency", + "columnName": "charisma_saving_throw_proficiency", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "armorType", + "columnName": "armor_type", + "affinity": "TEXT", + "defaultValue": "'none'" + }, + { + "fieldPath": "shieldBonus", + "columnName": "shield_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "naturalArmorBonus", + "columnName": "natural_armor_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "otherArmorDescription", + "columnName": "other_armor_description", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "hitDice", + "columnName": "hit_dice", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "hasCustomHP", + "columnName": "has_custom_hit_points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customHPDescription", + "columnName": "custom_hit_points_description", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "walkSpeed", + "columnName": "walk_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "burrowSpeed", + "columnName": "burrow_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "climbSpeed", + "columnName": "climb_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "flySpeed", + "columnName": "fly_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "canHover", + "columnName": "can_hover", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "swimSpeed", + "columnName": "swim_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasCustomSpeed", + "columnName": "has_custom_speed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "customSpeedDescription", + "columnName": "custom_speed_description", + "affinity": "TEXT" + }, + { + "fieldPath": "challengeRating", + "columnName": "challenge_rating", + "affinity": "TEXT", + "defaultValue": "'1'" + }, + { + "fieldPath": "customChallengeRatingDescription", + "columnName": "custom_challenge_rating_description", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "customProficiencyBonus", + "columnName": "custom_proficiency_bonus", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "telepathyRange", + "columnName": "telepathy_range", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "understandsButDescription", + "columnName": "understands_but_description", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "senses", + "columnName": "senses", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "skills", + "columnName": "skills", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "damageImmunities", + "columnName": "damage_immunities", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "damageResistances", + "columnName": "damage_resistances", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "damageVulnerabilities", + "columnName": "damage_vulnerabilities", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "conditionImmunities", + "columnName": "condition_immunities", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "languages", + "columnName": "languages", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "abilities", + "columnName": "abilities", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "lairActions", + "columnName": "lair_actions", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "legendaryActions", + "columnName": "legendary_actions", + "affinity": "TEXT", + "defaultValue": "'[]'" + }, + { + "fieldPath": "regionalActions", + "columnName": "regional_actions", + "affinity": "TEXT", + "defaultValue": "'[]'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "monsters_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "subtype", + "columnName": "subtype", + "affinity": "TEXT" + }, + { + "fieldPath": "alignment", + "columnName": "alignment", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "monsters", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_UPDATE BEFORE UPDATE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_BEFORE_DELETE BEFORE DELETE ON `monsters` BEGIN DELETE FROM `monsters_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_UPDATE AFTER UPDATE ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_monsters_fts_AFTER_INSERT AFTER INSERT ON `monsters` BEGIN INSERT INTO `monsters_fts`(`docid`, `name`, `size`, `type`, `subtype`, `alignment`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`size`, NEW.`type`, NEW.`subtype`, NEW.`alignment`); END" + ] + }, + { + "tableName": "collections", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "collection_monsters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collection_id` TEXT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`collection_id`) REFERENCES `collections`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collection_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "monsterId", + "columnName": "monster_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ordinal", + "columnName": "ordinal", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_monsters_collection_id", + "unique": false, + "columnNames": [ + "collection_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_collection_id` ON `${TABLE_NAME}` (`collection_id`)" + }, + { + "name": "index_collection_monsters_monster_id", + "unique": false, + "columnNames": [ + "monster_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)" + } + ], + "foreignKeys": [ + { + "table": "collections", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collection_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "monsters", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "monster_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "dashboard_monsters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "monsterId", + "columnName": "monster_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ordinal", + "columnName": "ordinal", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_dashboard_monsters_monster_id", + "unique": false, + "columnNames": [ + "monster_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `${TABLE_NAME}` (`monster_id`)" + } + ], + "foreignKeys": [ + { + "table": "monsters", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "monster_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6d497d0f0530c0f46bed1d57492e940e')" + ] + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java b/Android/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java index 3bfdfb7..d8e8a16 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/AppDatabase.java @@ -5,6 +5,7 @@ import androidx.room.RoomDatabase; import androidx.room.TypeConverters; import com.majinnaibu.monstercards.data.CollectionDAO; +import com.majinnaibu.monstercards.data.DashboardDAO; import com.majinnaibu.monstercards.data.MonsterDAO; import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter; import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter; @@ -15,10 +16,11 @@ import com.majinnaibu.monstercards.data.converters.SetOfStringConverter; import com.majinnaibu.monstercards.data.converters.UUIDConverter; import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.CollectionMonster; +import com.majinnaibu.monstercards.models.DashboardMonster; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.MonsterFTS; -@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class}, version = 4) +@Database(entities = {Monster.class, MonsterFTS.class, Collection.class, CollectionMonster.class, DashboardMonster.class}, version = 5) @TypeConverters({ ArmorTypeConverter.class, ChallengeRatingConverter.class, @@ -31,4 +33,5 @@ import com.majinnaibu.monstercards.models.MonsterFTS; public abstract class AppDatabase extends RoomDatabase { public abstract MonsterDAO monsterDAO(); public abstract CollectionDAO collectionDAO(); + public abstract DashboardDAO dashboardDAO(); } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java b/Android/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java index 67d94ad..dcea2c4 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MonsterCardsApplication.java @@ -44,6 +44,14 @@ public class MonsterCardsApplication extends Application { database.execSQL("CREATE INDEX IF NOT EXISTS `index_collection_monsters_monster_id` ON `collection_monsters` (`monster_id`)"); } }; + private static final Migration MIGRATION_4_5 = new Migration(4, 5) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `dashboard_monsters` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `monster_id` TEXT NOT NULL, `ordinal` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`monster_id`) REFERENCES `monsters`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE INDEX IF NOT EXISTS `index_dashboard_monsters_monster_id` ON `dashboard_monsters` (`monster_id`)"); + database.execSQL("INSERT INTO dashboard_monsters(monster_id, ordinal) SELECT id, 0 FROM monsters"); + } + }; private MonsterRepository m_monsterLibraryRepository; @@ -66,6 +74,7 @@ public class MonsterCardsApplication extends Application { .addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_2_3) .addMigrations(MIGRATION_3_4) + .addMigrations(MIGRATION_4_5) .fallbackToDestructiveMigrationOnDowngrade() // .fallbackToDestructiveMigration() .build(); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/DashboardDAO.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/DashboardDAO.java new file mode 100644 index 0000000..ce4fc6a --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/DashboardDAO.java @@ -0,0 +1,39 @@ +package com.majinnaibu.monstercards.data; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Update; + +import com.majinnaibu.monstercards.models.DashboardMonster; +import com.majinnaibu.monstercards.models.Monster; + +import java.util.List; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; + +@Dao +public interface DashboardDAO { + + @Query("SELECT monsters.* FROM monsters INNER JOIN dashboard_monsters ON monsters.id = dashboard_monsters.monster_id ORDER BY dashboard_monsters.ordinal ASC, dashboard_monsters.id ASC") + Flowable> getDashboardMonsters(); + + @Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC") + Flowable> getDashboardMonsterEntries(); + + @Insert + Completable addMonsterToDashboard(DashboardMonster... dashboardMonsters); + + @Query("DELETE FROM dashboard_monsters WHERE id = :id") + Completable removeDashboardMonsterById(long id); + + @Query("DELETE FROM dashboard_monsters WHERE monster_id = :monsterId") + Completable removeMonsterFromDashboard(String monsterId); + + @Query("DELETE FROM dashboard_monsters") + Completable clearDashboard(); + + @Update + Completable updateDashboardMonsters(List dashboardMonsters); +} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java index 09d95cb..ab1e81d 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/MonsterRepository.java @@ -7,6 +7,7 @@ import com.majinnaibu.monstercards.helpers.StringHelper; import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.CollectionMonster; import com.majinnaibu.monstercards.models.CollectionWithCount; +import com.majinnaibu.monstercards.models.DashboardMonster; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.SearchResultItem; @@ -197,6 +198,68 @@ public class MonsterRepository { return result; } + public Flowable> getDashboardMonsters() { + return m_db.dashboardDAO() + .getDashboardMonsters() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Flowable> getDashboardMonsterEntries() { + return m_db.dashboardDAO() + .getDashboardMonsterEntries() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Completable addMonsterToDashboard(@NonNull UUID monsterId) { + DashboardMonster entry = new DashboardMonster(monsterId, 0); + Completable result = m_db.dashboardDAO().addMonsterToDashboard(entry); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + public Completable addCollectionToDashboard(@NonNull UUID collectionId) { + return m_db.collectionDAO().getMonstersForCollection(collectionId.toString()) + .firstOrError() + .flatMapCompletable(monsters -> { + if (monsters.isEmpty()) { + return Completable.complete(); + } + DashboardMonster[] entries = new DashboardMonster[monsters.size()]; + for (int i = 0; i < monsters.size(); i++) { + entries[i] = new DashboardMonster(monsters.get(i).id, i); + } + return m_db.dashboardDAO().addMonsterToDashboard(entries); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Completable removeMonsterFromDashboard(@NonNull UUID monsterId) { + Completable result = m_db.dashboardDAO().removeMonsterFromDashboard(monsterId.toString()); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + public Completable removeDashboardMonsterById(long id) { + Completable result = m_db.dashboardDAO().removeDashboardMonsterById(id); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + public Completable clearDashboard() { + Completable result = m_db.dashboardDAO().clearDashboard(); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + + public Completable updateDashboardMonsters(List items) { + Completable result = m_db.dashboardDAO().updateDashboardMonsters(items); + result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + return result; + } + private static class Helpers { static boolean monsterMatchesSearch(Monster monster, String searchText) { if (StringHelper.isNullOrEmpty(searchText)) { diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/DashboardMonster.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/DashboardMonster.java new file mode 100644 index 0000000..414fb9e --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/DashboardMonster.java @@ -0,0 +1,43 @@ +package com.majinnaibu.monstercards.models; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import java.util.UUID; + +@Entity( + tableName = "dashboard_monsters", + foreignKeys = @ForeignKey( + entity = Monster.class, + parentColumns = "id", + childColumns = "monster_id", + onDelete = ForeignKey.CASCADE + ), + indices = @Index(value = {"monster_id"}) +) +public class DashboardMonster { + + @PrimaryKey(autoGenerate = true) + public long id; + + @NonNull + @ColumnInfo(name = "monster_id") + public UUID monsterId; + + @ColumnInfo(name = "ordinal", defaultValue = "0") + public int ordinal; + + public DashboardMonster() { + this.monsterId = UUID.fromString("00000000-0000-0000-0000-000000000000"); + this.ordinal = 0; + } + + public DashboardMonster(@NonNull UUID monsterId, int ordinal) { + this.monsterId = monsterId; + this.ordinal = ordinal; + } +} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java index 0ac3180..911696c 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java @@ -4,6 +4,9 @@ import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; @@ -44,6 +47,7 @@ public class CollectionDetailFragment extends MCFragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + setHasOptionsMenu(true); View root = inflater.inflate(R.layout.fragment_collection_detail, container, false); Bundle arguments = getArguments(); @@ -172,6 +176,36 @@ public class CollectionDetailFragment extends MCFragment { Navigation.findNavController(requireView()).navigate(action); } + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.collection_detail_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) { + addCollectionToDashboard(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void addCollectionToDashboard() { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.addCollectionToDashboard(mCollectionId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + Collection collection = mViewModel.getCollection().getValue(); + String collectionName = collection != null ? collection.name : ""; + Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collectionName), Snackbar.LENGTH_LONG).show(); + } + }, Logger::logError)); + } + @Override public void onDestroyView() { super.onDestroyView(); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java index 92e61ce..67b7810 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java @@ -3,52 +3,77 @@ package com.majinnaibu.monstercards.ui.dashboard; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavDirections; import androidx.navigation.Navigation; import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.utils.Logger; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; public class DashboardFragment extends MCFragment { private DashboardViewModel mViewModel; private ViewHolder mHolder; private DashboardRecyclerViewAdapter mAdapter; + private final CompositeDisposable mDisposables = new CompositeDisposable(); + @Nullable + @Override public View onCreateView(@NonNull LayoutInflater inflater, - ViewGroup container, Bundle savedInstanceState) { + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + setHasOptionsMenu(true); mViewModel = new ViewModelProvider(this).get(DashboardViewModel.class); View root = inflater.inflate(R.layout.fragment_dashboard, container, false); mHolder = new ViewHolder(root); - setupRecyclerView(mHolder.list); + FloatingActionButton fab = root.findViewById(R.id.fab_add_to_dashboard); + if (fab != null) { + fab.setOnClickListener(v -> showAddToDashboardOptionsDialog()); + } - getMonsterRepository() - .getMonsters() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(monsters -> mViewModel.setMonsters(monsters)); + setupRecyclerView(mHolder.list); + loadDashboardMonsters(); return root; } + private void loadDashboardMonsters() { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.getDashboardMonsters() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(monsters -> mViewModel.setMonsters(monsters), Logger::logError)); + } + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { - int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 396); + int columnCount = Math.max(1, getResources().getConfiguration().screenWidthDp / 360); Logger.logWTF(String.format(Locale.US, "Setting column count to %d", columnCount)); Context context = requireContext(); GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount); @@ -66,6 +91,165 @@ public class DashboardFragment extends MCFragment { monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters)); } recyclerView.setAdapter(mAdapter); + + ItemTouchHelper.SimpleCallback touchHelperCallback = new ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT, + ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT + ) { + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + int fromPos = viewHolder.getAdapterPosition(); + int toPos = target.getAdapterPosition(); + if (fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION) { + List currentList = new ArrayList<>(mAdapter.getCurrentList()); + if (fromPos < currentList.size() && toPos < currentList.size()) { + Monster moved = currentList.remove(fromPos); + currentList.add(toPos, moved); + mAdapter.submitList(currentList); + } + } + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + List currentList = mAdapter.getCurrentList(); + if (position < currentList.size()) { + Monster monster = currentList.get(position); + removeMonsterFromDashboard(monster); + } + } + } + }; + new ItemTouchHelper(touchHelperCallback).attachToRecyclerView(recyclerView); + } + + private void showAddToDashboardOptionsDialog() { + String[] options = new String[]{ + getString(R.string.action_add_single_monster), + getString(R.string.action_add_collection_option), + getString(R.string.action_clear_dashboard) + }; + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_add_to_dashboard) + .setItems(options, (dialog, which) -> { + if (which == 0) { + showAddMonsterPicker(); + } else if (which == 1) { + showAddCollectionPicker(); + } else if (which == 2) { + clearDashboard(); + } + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + + private void showAddMonsterPicker() { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.getMonsters() + .firstOrError() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(monsters -> { + if (monsters.isEmpty()) { + View view = getView(); + if (view != null) { + Snackbar.make(view, getString(R.string.no_monsters_available), Snackbar.LENGTH_LONG).show(); + } + return; + } + String[] names = new String[monsters.size()]; + for (int i = 0; i < monsters.size(); i++) { + names[i] = monsters.get(i).name; + } + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.action_add_single_monster) + .setItems(names, (dialog, which) -> { + Monster selected = monsters.get(which); + addMonsterToDashboard(selected); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + }, Logger::logError)); + } + + private void showAddCollectionPicker() { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.getCollections() + .firstOrError() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(collections -> { + if (collections.isEmpty()) { + View view = getView(); + if (view != null) { + Snackbar.make(view, getString(R.string.no_collections_available), Snackbar.LENGTH_LONG).show(); + } + return; + } + String[] names = new String[collections.size()]; + for (int i = 0; i < collections.size(); i++) { + names[i] = collections.get(i).name; + } + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.action_add_collection_option) + .setItems(names, (dialog, which) -> { + Collection selected = collections.get(which); + addCollectionToDashboard(selected); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + }, Logger::logError)); + } + + private void addMonsterToDashboard(@NonNull Monster monster) { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.addMonsterToDashboard(monster.id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monster.name), Snackbar.LENGTH_LONG).show(); + } + }, Logger::logError)); + } + + private void addCollectionToDashboard(@NonNull Collection collection) { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.addCollectionToDashboard(collection.id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + Snackbar.make(view, getString(R.string.snackbar_collection_added_to_dashboard, collection.name), Snackbar.LENGTH_LONG).show(); + } + }, Logger::logError)); + } + + private void removeMonsterFromDashboard(@NonNull Monster monster) { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.removeMonsterFromDashboard(monster.id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> {}, Logger::logError)); + } + + private void clearDashboard() { + MonsterRepository repository = getMonsterRepository(); + mDisposables.add(repository.clearDashboard() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + Snackbar.make(view, getString(R.string.snackbar_dashboard_cleared), Snackbar.LENGTH_LONG).show(); + } + }, Logger::logError)); } private void navigateToMonsterDetail(Monster monster) { @@ -73,6 +257,33 @@ public class DashboardFragment extends MCFragment { Navigation.findNavController(requireView()).navigate(action); } + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.dashboard_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.menu_action_add_single_monster) { + showAddMonsterPicker(); + return true; + } else if (item.getItemId() == R.id.menu_action_add_collection_option) { + showAddCollectionPicker(); + return true; + } else if (item.getItemId() == R.id.menu_action_clear_dashboard) { + clearDashboard(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mDisposables.clear(); + } + private static class ViewHolder { final RecyclerView list; @@ -80,5 +291,4 @@ public class DashboardFragment extends MCFragment { list = root.findViewById(R.id.list); } } - } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java index 0499a93..f6497a2 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java @@ -7,6 +7,7 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.Navigation; import androidx.recyclerview.widget.DividerItemDecoration; @@ -19,6 +20,7 @@ import com.google.android.material.snackbar.Snackbar; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.models.Monster; +import com.majinnaibu.monstercards.ui.monster.MonsterDetailFragmentDirections; import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; import com.majinnaibu.monstercards.utils.Logger; @@ -92,23 +94,16 @@ public class LibraryFragment extends MCFragment { new DisposableCompletableObserver() { @Override public void onComplete() { - View view = getView(); - assert view != null; - Snackbar.make( - view, - getString(R.string.snackbar_monster_created, monster.name), - Snackbar.LENGTH_LONG) - .setAction("Action", (_view) -> navigateToMonsterDetail(monster.id)) - .show(); + navigateToEditMonster(monster.id); } @Override public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { Logger.logError("Error creating monster", e); View view = getView(); - assert view != null; - Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG) - .setAction("Action", null).show(); + if (view != null) { + Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show(); + } } }); }); @@ -118,4 +113,12 @@ public class LibraryFragment extends MCFragment { NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString()); Navigation.findNavController(requireView()).navigate(action); } + + protected void navigateToEditMonster(@NonNull UUID monsterId) { + NavController navController = Navigation.findNavController(requireView()); + NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString()); + navController.navigate(action); + action = MonsterDetailFragmentDirections.actionNavigationMonsterToEditMonsterFragment(monsterId.toString()); + navController.navigate(action); + } } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java index 0c9cbfe..9158747 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/monster/MonsterDetailFragment.java @@ -181,10 +181,31 @@ public class MonsterDetailFragment extends MCFragment { } else if (item.getItemId() == R.id.menu_action_add_to_collection) { showAddToCollectionDialog(); return true; + } else if (item.getItemId() == R.id.menu_action_add_to_dashboard) { + addCurrentMonsterToDashboard(); + return true; } return super.onOptionsItemSelected(item); } + private void addCurrentMonsterToDashboard() { + UUID monsterId = mViewModel.getId().getValue(); + if (monsterId == null) { + return; + } + MonsterRepository repository = getMonsterRepository(); + repository.addMonsterToDashboard(monsterId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + View view = getView(); + if (view != null) { + String monsterName = mViewModel.getName().getValue(); + Snackbar.make(view, getString(R.string.snackbar_added_to_dashboard, monsterName), Snackbar.LENGTH_LONG).show(); + } + }, Logger::logError); + } + private void showAddToCollectionDialog() { UUID monsterId = mViewModel.getId().getValue(); if (monsterId == null) { diff --git a/Android/app/src/main/res/layout/fragment_dashboard.xml b/Android/app/src/main/res/layout/fragment_dashboard.xml index bb08ff8..d09e2f5 100644 --- a/Android/app/src/main/res/layout/fragment_dashboard.xml +++ b/Android/app/src/main/res/layout/fragment_dashboard.xml @@ -20,4 +20,16 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/card_monster" /> + + diff --git a/Android/app/src/main/res/menu/collection_detail_menu.xml b/Android/app/src/main/res/menu/collection_detail_menu.xml new file mode 100644 index 0000000..464f7b4 --- /dev/null +++ b/Android/app/src/main/res/menu/collection_detail_menu.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/Android/app/src/main/res/menu/dashboard_menu.xml b/Android/app/src/main/res/menu/dashboard_menu.xml new file mode 100644 index 0000000..81b0839 --- /dev/null +++ b/Android/app/src/main/res/menu/dashboard_menu.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/Android/app/src/main/res/menu/monster_detail_menu.xml b/Android/app/src/main/res/menu/monster_detail_menu.xml index b11b542..0dc658c 100644 --- a/Android/app/src/main/res/menu/monster_detail_menu.xml +++ b/Android/app/src/main/res/menu/monster_detail_menu.xml @@ -3,14 +3,17 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + android:id="@+id/menu_action_edit_monster" + android:title="@string/action_edit" + app:showAsAction="never" /> + android:id="@+id/menu_action_add_to_collection" + android:title="@string/action_add_to_collection" + app:showAsAction="never" /> + + diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index 2c12787..2c57222 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -152,4 +152,14 @@ No collections available. Create one first! 1 monster %d monsters + Add to Dashboard + Add Collection to Dashboard + Add Monster + Add Collection + Clear Dashboard + Add to Dashboard + Added %1$s to Dashboard + Added collection %1$s to Dashboard + Dashboard cleared + No monsters available in library. Create one first!