Makes the dashboard editable.

This commit is contained in:
2026-09-07 15:44:05 -07:00
parent e2a1fafaa9
commit edc6415ad5
15 changed files with 1116 additions and 29 deletions

View File

@@ -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')"
]
}
}

View File

@@ -5,6 +5,7 @@ import androidx.room.RoomDatabase;
import androidx.room.TypeConverters; import androidx.room.TypeConverters;
import com.majinnaibu.monstercards.data.CollectionDAO; import com.majinnaibu.monstercards.data.CollectionDAO;
import com.majinnaibu.monstercards.data.DashboardDAO;
import com.majinnaibu.monstercards.data.MonsterDAO; import com.majinnaibu.monstercards.data.MonsterDAO;
import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter; import com.majinnaibu.monstercards.data.converters.ArmorTypeConverter;
import com.majinnaibu.monstercards.data.converters.ChallengeRatingConverter; 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.data.converters.UUIDConverter;
import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster; import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.MonsterFTS; 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({ @TypeConverters({
ArmorTypeConverter.class, ArmorTypeConverter.class,
ChallengeRatingConverter.class, ChallengeRatingConverter.class,
@@ -31,4 +33,5 @@ import com.majinnaibu.monstercards.models.MonsterFTS;
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract MonsterDAO monsterDAO(); public abstract MonsterDAO monsterDAO();
public abstract CollectionDAO collectionDAO(); public abstract CollectionDAO collectionDAO();
public abstract DashboardDAO dashboardDAO();
} }

View File

@@ -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`)"); 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; private MonsterRepository m_monsterLibraryRepository;
@@ -66,6 +74,7 @@ public class MonsterCardsApplication extends Application {
.addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3) .addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4) .addMigrations(MIGRATION_3_4)
.addMigrations(MIGRATION_4_5)
.fallbackToDestructiveMigrationOnDowngrade() .fallbackToDestructiveMigrationOnDowngrade()
// .fallbackToDestructiveMigration() // .fallbackToDestructiveMigration()
.build(); .build();

View File

@@ -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<List<Monster>> getDashboardMonsters();
@Query("SELECT * FROM dashboard_monsters ORDER BY ordinal ASC, id ASC")
Flowable<List<DashboardMonster>> 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<DashboardMonster> dashboardMonsters);
}

View File

@@ -7,6 +7,7 @@ import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Collection; import com.majinnaibu.monstercards.models.Collection;
import com.majinnaibu.monstercards.models.CollectionMonster; import com.majinnaibu.monstercards.models.CollectionMonster;
import com.majinnaibu.monstercards.models.CollectionWithCount; import com.majinnaibu.monstercards.models.CollectionWithCount;
import com.majinnaibu.monstercards.models.DashboardMonster;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SearchResultItem; import com.majinnaibu.monstercards.models.SearchResultItem;
@@ -197,6 +198,68 @@ public class MonsterRepository {
return result; return result;
} }
public Flowable<List<Monster>> getDashboardMonsters() {
return m_db.dashboardDAO()
.getDashboardMonsters()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<DashboardMonster>> 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<DashboardMonster> items) {
Completable result = m_db.dashboardDAO().updateDashboardMonsters(items);
result.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
return result;
}
private static class Helpers { private static class Helpers {
static boolean monsterMatchesSearch(Monster monster, String searchText) { static boolean monsterMatchesSearch(Monster monster, String searchText) {
if (StringHelper.isNullOrEmpty(searchText)) { if (StringHelper.isNullOrEmpty(searchText)) {

View File

@@ -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;
}
}

View File

@@ -4,6 +4,9 @@ import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
@@ -44,6 +47,7 @@ public class CollectionDetailFragment extends MCFragment {
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 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); View root = inflater.inflate(R.layout.fragment_collection_detail, container, false);
Bundle arguments = getArguments(); Bundle arguments = getArguments();
@@ -172,6 +176,36 @@ public class CollectionDetailFragment extends MCFragment {
Navigation.findNavController(requireView()).navigate(action); 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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();

View File

@@ -3,52 +3,77 @@ package com.majinnaibu.monstercards.ui.dashboard;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.R; 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.models.Monster;
import com.majinnaibu.monstercards.ui.shared.MCFragment; import com.majinnaibu.monstercards.ui.shared.MCFragment;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class DashboardFragment extends MCFragment { public class DashboardFragment extends MCFragment {
private DashboardViewModel mViewModel; private DashboardViewModel mViewModel;
private ViewHolder mHolder; private ViewHolder mHolder;
private DashboardRecyclerViewAdapter mAdapter; private DashboardRecyclerViewAdapter mAdapter;
private final CompositeDisposable mDisposables = new CompositeDisposable();
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, 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); mViewModel = new ViewModelProvider(this).get(DashboardViewModel.class);
View root = inflater.inflate(R.layout.fragment_dashboard, container, false); View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
mHolder = new ViewHolder(root); 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() setupRecyclerView(mHolder.list);
.getMonsters() loadDashboardMonsters();
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> mViewModel.setMonsters(monsters));
return root; 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) { 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)); Logger.logWTF(String.format(Locale.US, "Setting column count to %d", columnCount));
Context context = requireContext(); Context context = requireContext();
GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount); GridLayoutManager layoutManager = new GridLayoutManager(context, columnCount);
@@ -66,6 +91,165 @@ public class DashboardFragment extends MCFragment {
monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters)); monsterData.observe(getViewLifecycleOwner(), monsters -> mAdapter.submitList(monsters));
} }
recyclerView.setAdapter(mAdapter); 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<Monster> 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<Monster> 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) { private void navigateToMonsterDetail(Monster monster) {
@@ -73,6 +257,33 @@ public class DashboardFragment extends MCFragment {
Navigation.findNavController(requireView()).navigate(action); 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 { private static class ViewHolder {
final RecyclerView list; final RecyclerView list;
@@ -80,5 +291,4 @@ public class DashboardFragment extends MCFragment {
list = root.findViewById(R.id.list); list = root.findViewById(R.id.list);
} }
} }
} }

View File

@@ -7,6 +7,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration; 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.R;
import com.majinnaibu.monstercards.data.MonsterRepository; import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Monster; 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.MCFragment;
import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback;
import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.Logger;
@@ -92,23 +94,16 @@ public class LibraryFragment extends MCFragment {
new DisposableCompletableObserver() { new DisposableCompletableObserver() {
@Override @Override
public void onComplete() { public void onComplete() {
View view = getView(); navigateToEditMonster(monster.id);
assert view != null;
Snackbar.make(
view,
getString(R.string.snackbar_monster_created, monster.name),
Snackbar.LENGTH_LONG)
.setAction("Action", (_view) -> navigateToMonsterDetail(monster.id))
.show();
} }
@Override @Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
Logger.logError("Error creating monster", e); Logger.logError("Error creating monster", e);
View view = getView(); View view = getView();
assert view != null; if (view != null) {
Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG) Snackbar.make(view, getString(R.string.snackbar_failed_to_create_monster), Snackbar.LENGTH_LONG).show();
.setAction("Action", null).show(); }
} }
}); });
}); });
@@ -118,4 +113,12 @@ public class LibraryFragment extends MCFragment {
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString()); NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
Navigation.findNavController(requireView()).navigate(action); 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);
}
} }

View File

@@ -181,10 +181,31 @@ public class MonsterDetailFragment extends MCFragment {
} else if (item.getItemId() == R.id.menu_action_add_to_collection) { } else if (item.getItemId() == R.id.menu_action_add_to_collection) {
showAddToCollectionDialog(); showAddToCollectionDialog();
return true; return true;
} else if (item.getItemId() == R.id.menu_action_add_to_dashboard) {
addCurrentMonsterToDashboard();
return true;
} }
return super.onOptionsItemSelected(item); 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() { private void showAddToCollectionDialog() {
UUID monsterId = mViewModel.getId().getValue(); UUID monsterId = mViewModel.getId().getValue();
if (monsterId == null) { if (monsterId == null) {

View File

@@ -20,4 +20,16 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/card_monster" /> tools:listitem="@layout/card_monster" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_to_dashboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:contentDescription="@string/action_add_to_dashboard"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_action_add_collection_to_dashboard"
android:title="@string/action_add_collection_to_dashboard"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_action_add_single_monster"
android:title="@string/action_add_single_monster"
app:showAsAction="never" />
<item
android:id="@+id/menu_action_add_collection_option"
android:title="@string/action_add_collection_option"
app:showAsAction="never" />
<item
android:id="@+id/menu_action_clear_dashboard"
android:title="@string/action_clear_dashboard"
app:showAsAction="never" />
</menu>

View File

@@ -3,14 +3,17 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/menu_action_add_to_collection" android:id="@+id/menu_action_edit_monster"
android:icon="@android:drawable/ic_input_add" android:title="@string/action_edit"
android:title="@string/action_add_to_collection" app:showAsAction="never" />
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/menu_action_edit_monster" android:id="@+id/menu_action_add_to_collection"
android:icon="@drawable/ic_edit_24" android:title="@string/action_add_to_collection"
android:title="@string/action_edit" app:showAsAction="never" />
app:showAsAction="always" />
<item
android:id="@+id/menu_action_add_to_dashboard"
android:title="@string/action_add_to_dashboard"
app:showAsAction="never" />
</menu> </menu>

View File

@@ -152,4 +152,14 @@
<string name="no_collections_available">No collections available. Create one first!</string> <string name="no_collections_available">No collections available. Create one first!</string>
<string name="format_monster_count_one">1 monster</string> <string name="format_monster_count_one">1 monster</string>
<string name="format_monster_count_other">%d monsters</string> <string name="format_monster_count_other">%d monsters</string>
<string name="action_add_to_dashboard">Add to Dashboard</string>
<string name="action_add_collection_to_dashboard">Add Collection to Dashboard</string>
<string name="action_add_single_monster">Add Monster</string>
<string name="action_add_collection_option">Add Collection</string>
<string name="action_clear_dashboard">Clear Dashboard</string>
<string name="dialog_add_to_dashboard">Add to Dashboard</string>
<string name="snackbar_added_to_dashboard">Added %1$s to Dashboard</string>
<string name="snackbar_collection_added_to_dashboard">Added collection %1$s to Dashboard</string>
<string name="snackbar_dashboard_cleared">Dashboard cleared</string>
<string name="no_monsters_available">No monsters available in library. Create one first!</string>
</resources> </resources>