From 79a36fa9d987954756ca92cd6402534eddfcf0f6 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Tue, 9 Jan 2024 18:25:59 +0100 Subject: [PATCH] Add room database for data caching & is new feature. --- app/build.gradle.kts | 27 ++- .../1.json | 210 ++++++++++++++++++ .../2.json | 210 ++++++++++++++++++ .../data/database/CompanionDatabase.kt | 41 ++++ .../data/database/lexicon/LexiconDao.kt | 30 +++ .../data/database/lexicon/LexiconDbo.kt | 48 ++++ .../rplexicon/data/database/quest/QuestDao.kt | 26 +++ .../rplexicon/data/database/quest/QuestDbo.kt | 49 ++++ .../pixelized/rplexicon/data/model/Lexicon.kt | 6 +- .../pixelized/rplexicon/data/model/Quest.kt | 9 +- .../rplexicon/data/parser/LexiconParser.kt | 47 +++- .../rplexicon/data/parser/QuestParser.kt | 82 +++++-- .../rplexicon/data/parser/SheetParserScope.kt | 10 +- .../rplexicon/data/parser/TimeUpdateParser.kt | 13 ++ .../repository/lexicon/LexiconRepository.kt | 68 +++++- .../repository/lexicon/QuestRepository.kt | 59 ++++- .../lexicon/detail/LexiconDetailViewModel.kt | 58 +++-- .../ui/screens/lexicon/list/LexiconItem.kt | 3 + .../screens/lexicon/list/LexiconViewModel.kt | 3 +- .../quest/detail/QuestDetailViewModel.kt | 59 +++-- .../ui/screens/quest/list/QuestItem.kt | 2 + .../screens/quest/list/QuestListViewModel.kt | 1 + build.gradle.kts | 2 +- 23 files changed, 974 insertions(+), 89 deletions(-) create mode 100644 app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json create mode 100644 app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/2.json create mode 100644 app/src/main/java/com/pixelized/rplexicon/data/database/CompanionDatabase.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDao.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDbo.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDao.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDbo.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/data/parser/TimeUpdateParser.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e063b3d..cfe3a58 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,14 @@ +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.dagger.hilt.android") id("com.google.gms.google-services") id("com.google.firebase.crashlytics") + id("androidx.room") id("com.google.devtools.ksp") } @@ -19,7 +24,8 @@ android { keyPassword = "123456" } create("pixelized") { - storeFile = (project.properties["PIXELIZED_RELEASE_STORE_FILE"] as? String)?.let { file(it) } + storeFile = + (project.properties["PIXELIZED_RELEASE_STORE_FILE"] as? String)?.let { file(it) } storePassword = project.properties["PIXELIZED_RELEASE_STORE_PASSWORD"] as? String keyAlias = project.properties["PIXELIZED_RELEASE_KEY_ALIAS"] as? String keyPassword = project.properties["PIXELIZED_RELEASE_KEY_PASSWORD"] as? String @@ -64,6 +70,10 @@ android { } } + buildTypes.onEach { + it.buildConfigField("String", "DEFAULT_READ_TIME_STAMP", "\"$defaultReadTimestamp\"") + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -141,6 +151,11 @@ dependencies { ksp("com.google.dagger:hilt-android-compiler:2.50") ksp("com.google.dagger:hilt-compiler:2.50") + // Room + implementation("androidx.room:room-runtime:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") + ksp("androidx.room:room-compiler:2.6.1") + // Image implementation("io.coil-kt:coil-compose:2.5.0") } @@ -155,6 +170,16 @@ kotlin { jvmToolchain(17) } +room { + schemaDirectory("$projectDir/schemas") +} + +val defaultReadTimestamp: String + get() { + val formatter = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.FRANCE) + return formatter.format(Date()).toString() + } + val gitBuildNumber: Int get() { val stdout = org.apache.commons.io.output.ByteArrayOutputStream() diff --git a/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json b/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json new file mode 100644 index 0000000..26c295d --- /dev/null +++ b/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json @@ -0,0 +1,210 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "f2d0339fd127a0e9f6e2e816647d9ea9", + "entities": [ + { + "tableName": "lexicon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `category` TEXT, `diminutive` TEXT, `gender` TEXT, `race` TEXT, `status` TEXT, `location` TEXT, `portrait` TEXT, `description` TEXT, `history` TEXT, `tags` TEXT, `lastUpdated` INTEGER, `lastRead` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "diminutive", + "columnName": "diminutive", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "race", + "columnName": "race", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "portrait", + "columnName": "portrait", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "history", + "columnName": "history", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "quest", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category` TEXT, `title` TEXT NOT NULL, `subTitle` TEXT, `completed` INTEGER NOT NULL, `questGiver` TEXT, `area` TEXT, `groupReward` TEXT, `individualReward` TEXT, `description` TEXT NOT NULL, `illustrations` TEXT, `background` TEXT, `lastUpdated` INTEGER, `lastRead` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subTitle", + "columnName": "subTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questGiver", + "columnName": "questGiver", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "area", + "columnName": "area", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupReward", + "columnName": "groupReward", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "individualReward", + "columnName": "individualReward", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "illustrations", + "columnName": "illustrations", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "background", + "columnName": "background", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "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, 'f2d0339fd127a0e9f6e2e816647d9ea9')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/2.json b/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/2.json new file mode 100644 index 0000000..50dbe05 --- /dev/null +++ b/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/2.json @@ -0,0 +1,210 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "f2d0339fd127a0e9f6e2e816647d9ea9", + "entities": [ + { + "tableName": "lexicon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `category` TEXT, `diminutive` TEXT, `gender` TEXT, `race` TEXT, `status` TEXT, `location` TEXT, `portrait` TEXT, `description` TEXT, `history` TEXT, `tags` TEXT, `lastUpdated` INTEGER, `lastRead` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "diminutive", + "columnName": "diminutive", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "race", + "columnName": "race", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "portrait", + "columnName": "portrait", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "history", + "columnName": "history", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "quest", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category` TEXT, `title` TEXT NOT NULL, `subTitle` TEXT, `completed` INTEGER NOT NULL, `questGiver` TEXT, `area` TEXT, `groupReward` TEXT, `individualReward` TEXT, `description` TEXT NOT NULL, `illustrations` TEXT, `background` TEXT, `lastUpdated` INTEGER, `lastRead` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subTitle", + "columnName": "subTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questGiver", + "columnName": "questGiver", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "area", + "columnName": "area", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupReward", + "columnName": "groupReward", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "individualReward", + "columnName": "individualReward", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "illustrations", + "columnName": "illustrations", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "background", + "columnName": "background", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "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, 'f2d0339fd127a0e9f6e2e816647d9ea9')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/CompanionDatabase.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/CompanionDatabase.kt new file mode 100644 index 0000000..0eb9a93 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/CompanionDatabase.kt @@ -0,0 +1,41 @@ +package com.pixelized.rplexicon.data.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.pixelized.rplexicon.data.database.lexicon.LexiconDao +import com.pixelized.rplexicon.data.database.lexicon.LexiconDbo +import com.pixelized.rplexicon.data.database.quest.QuestDao +import com.pixelized.rplexicon.data.database.quest.QuestDbo +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Database( + entities = [LexiconDbo::class, QuestDbo::class], + version = 2, + exportSchema = true, +) +abstract class CompanionDatabase : RoomDatabase() { + abstract fun lexiconDao(): LexiconDao + abstract fun questsDao(): QuestDao +} + +@Module +@InstallIn(SingletonComponent::class) +class DatabaseModule { + + @Provides + fun provideCompanionDatabase( + @ApplicationContext context: Context, + ): CompanionDatabase { + return synchronized(this) { + Room.databaseBuilder(context, CompanionDatabase::class.java, "companion_database") + .fallbackToDestructiveMigration() + .build() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDao.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDao.kt new file mode 100644 index 0000000..db9fcb5 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDao.kt @@ -0,0 +1,30 @@ +package com.pixelized.rplexicon.data.database.lexicon + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface LexiconDao { + + @Query("SELECT * from lexicon") + fun getAllFlow(): Flow> + + @Query("SELECT * from lexicon WHERE id = :id") + fun getByIdFlow(id: String): Flow + + @Query("SELECT id from lexicon WHERE name = :name LIMIT 1") + suspend fun getIdByName(name: String): String? + + @Insert(entity = LexiconDbo::class, onConflict = OnConflictStrategy.IGNORE) + fun insert(item: LexiconDataDbo) + + @Update(entity = LexiconDbo::class) + fun update(item: LexiconDataDbo): Int + + @Update(entity = LexiconDbo::class) + suspend fun update(item: LexiconReadTimestampDbo): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDbo.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDbo.kt new file mode 100644 index 0000000..7acef41 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/lexicon/LexiconDbo.kt @@ -0,0 +1,48 @@ +package com.pixelized.rplexicon.data.database.lexicon + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "lexicon") +data class LexiconDbo( + @PrimaryKey + val id: String, + val name: String, + val category: String?, + val diminutive: String?, + val gender: String?, + val race: String?, + val status: String?, + val location: String?, + val portrait: String?, + val description: String?, + val history: String?, + val tags: String?, + val lastUpdated: Long?, + val lastRead: Long?, +) + +@Entity(tableName = "lexicon") +data class LexiconDataDbo( + @PrimaryKey + val id: String, + val name: String, + val category: String?, + val diminutive: String?, + val gender: String?, + val race: String?, + val status: String?, + val location: String?, + val portrait: String?, + val description: String?, + val history: String?, + val tags: String?, + val lastUpdated: Long?, +) + +@Entity(tableName = "lexicon") +data class LexiconReadTimestampDbo( + @PrimaryKey + val id: String, + val lastRead: Long?, +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDao.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDao.kt new file mode 100644 index 0000000..f4d9d0b --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDao.kt @@ -0,0 +1,26 @@ +package com.pixelized.rplexicon.data.database.quest + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface QuestDao { + @Query("SELECT * from quest") + fun getAllFlow(): Flow> + + @Query("SELECT * from quest WHERE title IN (SELECT title from quest WHERE id = :id)") + fun getByIdFlow(id: String): Flow> + + @Insert(entity = QuestDbo::class, onConflict = OnConflictStrategy.IGNORE) + fun insert(item: QuestDataDbo) + + @Update(entity = QuestDbo::class) + fun update(item: QuestDataDbo): Int + + @Update(entity = QuestDbo::class) + suspend fun update(item: QuestsReadTimestampDbo): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDbo.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDbo.kt new file mode 100644 index 0000000..bfcfd9b --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/quest/QuestDbo.kt @@ -0,0 +1,49 @@ +package com.pixelized.rplexicon.data.database.quest + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "quest") +data class QuestDbo( + @PrimaryKey + val id: String, + val category: String?, + val title: String, + val subTitle: String?, + val completed: Boolean, + val questGiver: String?, + val area: String?, + val groupReward: String?, + val individualReward: String?, + val description: String, + val illustrations: String?, + val background: String?, + val lastUpdated: Long?, + val lastRead: Long?, +) + +@Entity(tableName = "quest") +data class QuestDataDbo( + @PrimaryKey + val id: String, + val category: String?, + val title: String, + val subTitle: String?, + val completed: Boolean, + val questGiver: String?, + val area: String?, + val groupReward: String?, + val individualReward: String?, + val description: String, + val illustrations: String?, + val background: String?, + val lastUpdated: Long?, +) + +@Entity(tableName = "quest") +data class QuestsReadTimestampDbo( + @PrimaryKey + val id: String, + val lastRead: Long?, +) + diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/Lexicon.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/Lexicon.kt index a6244a5..1be9d91 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/model/Lexicon.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/Lexicon.kt @@ -17,4 +17,8 @@ data class Lexicon( val description: String?, val history: String?, val tags: String?, -) \ No newline at end of file + val lastUpdated: Long?, + val lastRead: Long, +) { + val isNew: Boolean get() = lastRead - (lastUpdated ?: 0) < 0 +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/Quest.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/Quest.kt index 08656cf..65e1408 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/model/Quest.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/Quest.kt @@ -8,15 +8,17 @@ data class Quest( val id: String, val category: String?, val title: String, + val lastRead: Long, val entries: List, ) { val complete = entries.all { it.complete } + val isNew = lastRead - entries.maxOf { it.lastUpdated ?: 0 } < 0 } @Stable data class QuestEntry( - val sheetIndex: Int, - val group: String?, + val id: String, + val category: String?, val title: String, val subtitle: String?, val complete: Boolean, @@ -25,6 +27,7 @@ data class QuestEntry( val groupReward: String?, val individualReward: String?, val description: String, - val images: List, + val illustrations: List, val background: Uri?, + val lastUpdated: Long?, ) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/LexiconParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/LexiconParser.kt index d8f8de6..3aeb6d9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/LexiconParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/LexiconParser.kt @@ -1,28 +1,31 @@ package com.pixelized.rplexicon.data.parser import com.google.api.services.sheets.v4.model.ValueRange +import com.pixelized.rplexicon.BuildConfig +import com.pixelized.rplexicon.data.database.lexicon.LexiconDataDbo +import com.pixelized.rplexicon.data.database.lexicon.LexiconDbo import com.pixelized.rplexicon.data.model.Lexicon import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import javax.inject.Inject class LexiconParser @Inject constructor( private val illustrationParser: IllustrationParser, + private val timeParser: TimeUpdateParser ) { @Throws(IncompatibleSheetStructure::class) - fun parse(sheet: ValueRange): List = parserScope { - val ids = hashMapOf() - val lexicons = mutableListOf() + fun parse(sheet: ValueRange): List = parserScope(timeParser) { + val lexicons = mutableListOf() sheet.forEachRowIndexed { index, row -> when (index) { 0 -> updateStructure(row = row, columns = COLUMNS) else -> { + val id = row.parse(column = ID) val name = row.parse(column = NAME) - if (name != null) { - ids[name] = ids.getOrDefault(name, 0) + 1 - val lexicon = Lexicon( - id = "$name-${ids[name]}", + if (id != null && name != null) { + val lexicon = LexiconDataDbo( + id = id, name = name, category = row.parse(column = CATEGORY), diminutive = row.parse(column = SHORT), @@ -30,10 +33,11 @@ class LexiconParser @Inject constructor( race = row.parse(column = RACE), status = row.parse(column = STATUS), location = row.parse(column = LOCATION), - portrait = illustrationParser.parse(row.parse(column = ILLUSTRATIONS)), + portrait = row.parse(column = ILLUSTRATIONS), description = row.parse(column = DESCRIPTION), history = row.parse(column = HISTORY), tags = row.parse(column = TAGS), + lastUpdated = row.parseTime(column = UPDATE), ) lexicons.add(lexicon) } @@ -44,7 +48,31 @@ class LexiconParser @Inject constructor( return@parserScope lexicons } + fun convert(data: List): List { + return data.map { convert(data = it) } + } + + fun convert(data: LexiconDbo): Lexicon { + return Lexicon( + id = data.id, + name = data.name, + category = data.category, + diminutive = data.diminutive, + gender = data.gender, + race = data.race, + status = data.status, + location = data.location, + portrait = illustrationParser.parse(value = data.portrait), + description = data.description, + history = data.history, + tags = data.tags, + lastUpdated = data.lastUpdated, + lastRead = data.lastRead ?: timeParser.parser(BuildConfig.DEFAULT_READ_TIME_STAMP) ?: 0, + ) + } + companion object { + private val ID = column("Id") private val NAME = column("Nom") private val CATEGORY = column("Catégorie") private val SHORT = column("Diminutif") @@ -56,9 +84,11 @@ class LexiconParser @Inject constructor( private val DESCRIPTION = column("Description") private val HISTORY = column("Histoire") private val TAGS = column("Mots clés") + private val UPDATE = column("Mise à jour") private val COLUMNS get() = listOf( + ID, NAME, CATEGORY, SHORT, @@ -70,6 +100,7 @@ class LexiconParser @Inject constructor( DESCRIPTION, HISTORY, TAGS, + UPDATE, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/QuestParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/QuestParser.kt index 59ac5d6..a3ee1f9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/QuestParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/QuestParser.kt @@ -2,57 +2,88 @@ package com.pixelized.rplexicon.data.parser import com.google.api.services.sheets.v4.model.ValueRange +import com.pixelized.rplexicon.BuildConfig +import com.pixelized.rplexicon.data.database.quest.QuestDataDbo +import com.pixelized.rplexicon.data.database.quest.QuestDbo import com.pixelized.rplexicon.data.model.Quest import com.pixelized.rplexicon.data.model.QuestEntry +import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull import javax.inject.Inject class QuestParser @Inject constructor( - private val imageParser: IllustrationParser + private val illustrationParser: IllustrationParser, + private val timeParser: TimeUpdateParser ) { - fun parse(sheet: ValueRange): List = parserScope { - val entries = hashMapOf>() + fun parse(sheet: ValueRange): List = parserScope(timeParser) { + val quests = mutableListOf() sheet.forEachRowIndexed { index, item -> when (index) { 0 -> updateStructure(row = item, columns = COLUMNS) + else -> { + val id = item.parse(column = ID) val quest = item.parse(column = TITLE) val description = item.parse(column = DESCRIPTION) - if (quest != null && description != null) { - val entry = QuestEntry( - sheetIndex = index, + if (id != null && quest != null && description != null) { + val entry = QuestDataDbo( + id = id, + category = item.parse(column = CATEGORY), title = quest, - group = item.parse(column = CATEGORY), - subtitle = item.parse(column = SUB_TITLE), - complete = item.parseBool(column = COMPLETED) ?: false, + subTitle = item.parse(column = SUB_TITLE), + completed = item.parseBool(column = COMPLETED) ?: false, questGiver = item.parse(column = QUEST_GIVER), area = item.parse(column = AREA), groupReward = item.parse(column = GROUP_REWARD), individualReward = item.parse(column = INDIVIDUAL_REWARD), description = description, - images = imageParser.parse(item.parse(column = ILLUSTRATIONS)), - background = item.parseUri(column = BACKGROUND), + illustrations = item.parse(column = ILLUSTRATIONS), + background = item.parse(column = BACKGROUND), + lastUpdated = item.parseTime(column = UPDATE), ) - entries.getOrPut(quest) { mutableListOf() }.add(entry) + quests.add(entry) } } } } - val quests = entries.keys.map { quest -> - val relatedEntries = entries[quest] ?: emptyList() - Quest( - id = "$quest-1", // TODO refactor that when quest have ids in the google sheet. - title = quest, - category = relatedEntries.firstNotNullOfOrNull { it.group }, - entries = relatedEntries, - ) - } - return@parserScope quests } + fun convert(data: List): List { + return data.groupBy { it.title }.mapNotNull { entry -> + entry.value.firstOrNull()?.let { main -> + Quest( + id = main.id, + category = main.category, + title = main.title, + lastRead = main.lastRead ?: timeParser.parser(BuildConfig.DEFAULT_READ_TIME_STAMP) ?: 0, + entries = entry.value.map { convert(data = it) }, + ) + } + } + } + + fun convert(data: QuestDbo): QuestEntry { + return QuestEntry( + id = data.id, + category = data.category, + title = data.title, + subtitle = data.subTitle, + complete = data.completed, + questGiver = data.questGiver, + area = data.area, + groupReward = data.groupReward, + individualReward = data.individualReward, + description = data.description, + illustrations = illustrationParser.parse(value = data.illustrations), + background = data.background?.toUriOrNull(), + lastUpdated = data.lastUpdated, + ) + } + companion object { + private val ID = column("Id") private val TITLE = column("Titre") private val CATEGORY = column("Catégorie") private val SUB_TITLE = column("Sous Titre") @@ -62,11 +93,13 @@ class QuestParser @Inject constructor( private val GROUP_REWARD = column("Récompense de groupe") private val INDIVIDUAL_REWARD = column("Récompense individuelle") private val DESCRIPTION = column("Description") - private val ILLUSTRATIONS = column("Image", "Illustrations") // TODO remove Image after 0.9.0 release - private val BACKGROUND = column("fond", "Fond") // TODO remove "fond" after 0.7.0 release + private val ILLUSTRATIONS = column("Illustrations") + private val BACKGROUND = column("Fond") + private val UPDATE = column("Mise à jour") private val COLUMNS get() = listOf( + ID, TITLE, CATEGORY, SUB_TITLE, @@ -78,6 +111,7 @@ class QuestParser @Inject constructor( DESCRIPTION, ILLUSTRATIONS, BACKGROUND, + UPDATE, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/SheetParserScope.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/SheetParserScope.kt index b0bd6a9..dcf9b59 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/SheetParserScope.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/SheetParserScope.kt @@ -7,12 +7,15 @@ import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure import com.pixelized.rplexicon.utilitary.extentions.sheet inline fun parserScope( + timeParser: TimeUpdateParser? = null, noinline block: SheetParserScope.() -> T ): T { - return SheetParserScope().parse(block) + return SheetParserScope(timeParser).parse(block) } -class SheetParserScope { +class SheetParserScope( + private val timeParser: TimeUpdateParser?, +) { private var structure: Map = hashMapOf() fun updateStructure(row: Any, columns: List) { @@ -81,6 +84,9 @@ class SheetParserScope { fun List<*>.parseUri(column: Column): Uri? = parse(column)?.takeIf { it.isNotBlank() }?.toUri() + fun List<*>.parseTime(column: Column): Long? = + parse(column = column)?.let { timeParser?.parser(value = it) } + fun List<*>.parseList(column: Column, separator: String = ","): List = parse(column) ?.takeIf { it.isNotBlank() } diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/TimeUpdateParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/TimeUpdateParser.kt new file mode 100644 index 0000000..7dc7791 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/TimeUpdateParser.kt @@ -0,0 +1,13 @@ +package com.pixelized.rplexicon.data.parser + +import java.text.SimpleDateFormat +import java.util.Locale +import javax.inject.Inject + +class TimeUpdateParser @Inject constructor() { + private val formatter = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.FRANCE) + + fun parser(value: String?): Long? { + return value?.let { formatter.parse(it) }?.time + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LexiconRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LexiconRepository.kt index 0a93bea..56b4958 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LexiconRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LexiconRepository.kt @@ -1,43 +1,101 @@ package com.pixelized.rplexicon.data.repository.lexicon +import com.pixelized.rplexicon.data.database.CompanionDatabase +import com.pixelized.rplexicon.data.database.lexicon.LexiconReadTimestampDbo import com.pixelized.rplexicon.data.model.Lexicon import com.pixelized.rplexicon.data.parser.LexiconParser import com.pixelized.rplexicon.data.repository.GoogleSheetServiceRepository import com.pixelized.rplexicon.data.repository.LexiconBinder import com.pixelized.rplexicon.utilitary.Update import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @Singleton class LexiconRepository @Inject constructor( private val googleRepository: GoogleSheetServiceRepository, + private val database: CompanionDatabase, private val lexiconParser: LexiconParser, ) { + private val scope = CoroutineScope(Dispatchers.Default + Job()) + private val _data = MutableStateFlow>(emptyList()) val data: StateFlow> get() = _data var lastSuccessFullUpdate: Update = Update.INITIAL private set - fun find(id: String?): Lexicon? { - return id?.let { _data.value.firstOrNull { item -> item.id == it } } + init { + scope.launch(Dispatchers.IO) { + database.lexiconDao().getAllFlow().collect { data -> + _data.value = lexiconParser.convert(data = data) + } + } } - fun findId(name: String?): String? { - return name?.let { _data.value.firstOrNull { item -> item.name == it }?.id } + /** + * Get a [Flow] of a nullable [Lexicon] instance in the list by filtering by id. + * @param id the id of the [Lexicon] instance. + * @return a [Flow] of a nullable [Lexicon] instance. + */ + fun getByIdFlow(id: String?): Flow = when (id) { + null -> emptyFlow() + else -> database.lexiconDao().getByIdFlow(id = id).map { + lexiconParser.convert(it) + } } + /** + * Find the first or null [Lexicon] instance in the list by filtering by name. + * @param name the name of the [Lexicon] instance. + * @return a nullable [Lexicon] instance. + */ + suspend fun getByNameFlow(name: String?): String? { + return name?.let { database.lexiconDao().getIdByName(name = name) } + } + + /** + * Query the [Lexicon] from the backend. + * @throws IncompatibleSheetStructure if the data structure change and mandatory data are missing. + * @throws Exception if other kind of exception happen, network for example. + */ @Throws(IncompatibleSheetStructure::class, Exception::class) suspend fun fetchLexicon() { googleRepository.fetch { sheet -> val request = sheet.get(LexiconBinder.ID, LexiconBinder.LEXICON) val data = lexiconParser.parse(sheet = request.execute()) - _data.tryEmit(data) + + database.lexiconDao().also { dao -> + data.forEach { + val row = dao.update(item = it) + if (row == 0) dao.insert(item = it) + } + } lastSuccessFullUpdate = Update.currentTime() } } + + /** + * Update the [Lexicon#lastTime] field of a [Lexicon] instance. + * @param id the id of the [Lexicon] instance. + * @param timestamp the timestamp that will update the lastRead filed. + */ + suspend fun updateReadTime(id: String, timestamp: Long = System.currentTimeMillis()) { + database.lexiconDao().update( + item = LexiconReadTimestampDbo( + id = id, + lastRead = timestamp, + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/QuestRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/QuestRepository.kt index 7bd1b25..60a770d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/QuestRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/QuestRepository.kt @@ -1,38 +1,91 @@ package com.pixelized.rplexicon.data.repository.lexicon +import com.pixelized.rplexicon.data.database.CompanionDatabase +import com.pixelized.rplexicon.data.database.quest.QuestsReadTimestampDbo import com.pixelized.rplexicon.data.model.Quest import com.pixelized.rplexicon.data.parser.QuestParser import com.pixelized.rplexicon.data.repository.GoogleSheetServiceRepository import com.pixelized.rplexicon.data.repository.LexiconBinder import com.pixelized.rplexicon.utilitary.Update import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @Singleton class QuestRepository @Inject constructor( private val googleRepository: GoogleSheetServiceRepository, + private val database: CompanionDatabase, private val questParser: QuestParser, ) { + private val scope = CoroutineScope(Dispatchers.Default + Job()) + private val _data = MutableStateFlow>(emptyList()) val data: StateFlow> get() = _data var lastSuccessFullUpdate: Update = Update.INITIAL private set - fun find(id: String?): Quest? { - return id?.let { data.value.firstOrNull { it.id == id } } + init { + scope.launch(Dispatchers.IO) { + database.questsDao().getAllFlow().collect { data -> + _data.value = questParser.convert(data = data) + } + } } + /** + * Find the first or null [Quest] instance in the database by filtering by id. + * @param id the id of the [Quest] instance. + * @return a nullable [Quest] instance. + */ + fun getByIdFlow(id: String?): Flow = when (id) { + null -> emptyFlow() + else -> database.questsDao().getByIdFlow(id = id).mapNotNull { + questParser.convert(it).firstOrNull() + } + } + + /** + * Query the [Quest] from the backend. + * @throws IncompatibleSheetStructure if the data structure change and mandatory data are missing. + * @throws Exception if other kind of exception happen, network for example. + */ @Throws(IncompatibleSheetStructure::class, Exception::class) suspend fun fetchQuests() { googleRepository.fetch { sheet -> val request = sheet.get(LexiconBinder.ID, LexiconBinder.QUEST_JOURNAL) val quests = questParser.parse(sheet = request.execute()) - _data.emit(quests) + + val dao = database.questsDao() + quests.forEach { + val row = dao.update(item = it) + if (row == 0) dao.insert(item = it) + } + lastSuccessFullUpdate = Update.currentTime() } } + + /** + * Update the [QuestBbo#lastTime] field of a [Quest] instance. + * @param id the id of the [Quest] instance. + * @param timestamp the timestamp that will update the lastRead filed. + */ + suspend fun updateReadTime(id: String, timestamp: Long = System.currentTimeMillis()) { + database.questsDao().update( + item = QuestsReadTimestampDbo( + id = id, + lastRead = timestamp, + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt index 564a707..4f7bb62 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt @@ -4,10 +4,14 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel @@ -16,7 +20,9 @@ class LexiconDetailViewModel @Inject constructor( lexiconRepository: LexiconRepository, characterSheetRepository: CharacterSheetRepository, ) : ViewModel() { - val haveCharacterSheet: State + + private val _haveCharacterSheet = mutableStateOf(false) + val haveCharacterSheet: State get() = _haveCharacterSheet private val _character = mutableStateOf(null) val character: State get() = _character @@ -25,24 +31,38 @@ class LexiconDetailViewModel @Inject constructor( init { val argument = savedStateHandle.lexiconDetailArgument - val source = lexiconRepository.find(id = argument.id) - if (source != null) { - _character.value = LexiconDetailUio( - name = source.name, - diminutive = source.diminutive?.let { "./ $it" }, - gender = source.gender, - race = source.race, - portrait = source.portrait, - status = source.status, - location = source.location, - description = source.description, - history = source.history, - tags = source.tags, - ) - } - haveCharacterSheet = mutableStateOf( - characterSheetRepository.find(name = source?.name) != null - ) + viewModelScope.launch { + launch(Dispatchers.IO) { + // update the last read time for that lexicon + lexiconRepository.updateReadTime(id = argument.id) + } + launch(Dispatchers.IO) { + lexiconRepository.getByIdFlow(id = argument.id).collect { source -> + // build the UI object. + val character = LexiconDetailUio( + name = source.name, + diminutive = source.diminutive?.let { "./ $it" }, + gender = source.gender, + race = source.race, + portrait = source.portrait, + status = source.status, + location = source.location, + description = source.description, + history = source.history, + tags = source.tags, + ) + // Check if we have a character sheet for that character. + val haveCharacterSheet = characterSheetRepository.find( + name = source.name, + ) != null + // Update the UI state + withContext(Dispatchers.Main) { + _character.value = character + _haveCharacterSheet.value = haveCharacterSheet + } + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconItem.kt index fd21124..e52951a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconItem.kt @@ -50,6 +50,7 @@ data class LexiconItemUio( val race: String?, val isPlayingCharacter: Boolean = false, val placeholder: Boolean = false, + val isNew: Boolean = false, ) { companion object { fun placeholder() = LexiconItemUio( @@ -93,6 +94,7 @@ fun LexiconItem( .padding(end = 4.dp) .alignByBaseline(), style = typography.base.titleMedium, + color = if (item.isNew) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, text = if (item.isPlayingCharacter) LOS_FULL else LOS_HOLLOW, ) @@ -206,6 +208,7 @@ private class LexiconItemPreviewProvider : PreviewParameterProvider - val location = locationRepository.find(id = entry.area) - QuestDetailUio.QuestStep( - subtitle = entry.subtitle, - giverId = lexiconRepository.findId(entry.questGiver), - giver = entry.questGiver, - placeId = location?.id, - place = location?.name ?: entry.area, - globalReward = entry.groupReward, - individualReward = entry.individualReward, - description = entry.description, - images = entry.images, + viewModelScope.launch { + launch(Dispatchers.IO) { + // update the last read time for that lexicon + questRepository.updateReadTime(id = argument.id) + } + launch(Dispatchers.IO) { + // fetch and display the detail data. + questRepository.getByIdFlow(id = argument.id).collect { source -> + val quest = QuestDetailUio( + id = source.id, + completed = source.entries.all { it.complete }, + background = source.entries.mapNotNull { it.background }.randomOrNull(), + title = source.title, + steps = source.entries.map { entry -> + val location = locationRepository.find(id = entry.area) + QuestDetailUio.QuestStep( + subtitle = entry.subtitle, + giverId = lexiconRepository.getByNameFlow(name = entry.questGiver), + giver = entry.questGiver, + placeId = location?.id, + place = location?.name ?: entry.area, + globalReward = entry.groupReward, + individualReward = entry.individualReward, + description = entry.description, + images = entry.illustrations, + ) + }, ) - }, - ) + // Update the UI state + withContext(Dispatchers.Main) { + _quest.value = quest + } + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestItem.kt index b362c61..88bc243 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestItem.kt @@ -40,6 +40,7 @@ data class QuestItemUio( val title: String, val complete: Boolean, val placeholder: Boolean = false, + val isNew: Boolean = false, ) { companion object { fun preview( @@ -77,6 +78,7 @@ fun QuestItem( Text( modifier = alignModifier, style = typography.base.titleMedium, + color = if (item.isNew) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, text = if (item.complete) LOS_FULL else LOS_HOLLOW, ) Text( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListViewModel.kt index 6061f58..4817e60 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListViewModel.kt @@ -59,6 +59,7 @@ class QuestListViewModel @Inject constructor( id = item.id, title = item.title, complete = item.complete, + isNew = item.isNew, ) }, ) diff --git a/build.gradle.kts b/build.gradle.kts index 40c8efe..9cc6f79 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,6 @@ plugins { id("com.google.gms.google-services") version "4.3.14" apply false id("com.google.dagger.hilt.android") version "2.50" apply false id("com.google.firebase.crashlytics") version "2.9.7" apply false - id("org.jetbrains.kotlin.kapt") version "1.9.10" apply false + id("androidx.room") version "2.6.0" apply false id("com.google.devtools.ksp") version "1.9.21-1.0.16" apply false }