Add room database for data caching & is new feature.
This commit is contained in:
		
							parent
							
								
									a2f26cc2a5
								
							
						
					
					
						commit
						79a36fa9d9
					
				
					 23 changed files with 974 additions and 89 deletions
				
			
		| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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')"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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')"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<List<LexiconDbo>>
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT * from lexicon WHERE id = :id")
 | 
			
		||||
    fun getByIdFlow(id: String): Flow<LexiconDbo>
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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?,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -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<List<QuestDbo>>
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT * from quest WHERE title IN (SELECT title from quest WHERE id = :id)")
 | 
			
		||||
    fun getByIdFlow(id: String): Flow<List<QuestDbo>>
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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?,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,4 +17,8 @@ data class Lexicon(
 | 
			
		|||
    val description: String?,
 | 
			
		||||
    val history: String?,
 | 
			
		||||
    val tags: String?,
 | 
			
		||||
)
 | 
			
		||||
    val lastUpdated: Long?,
 | 
			
		||||
    val lastRead: Long,
 | 
			
		||||
) {
 | 
			
		||||
    val isNew: Boolean get() = lastRead - (lastUpdated ?: 0) < 0
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,15 +8,17 @@ data class Quest(
 | 
			
		|||
    val id: String,
 | 
			
		||||
    val category: String?,
 | 
			
		||||
    val title: String,
 | 
			
		||||
    val lastRead: Long,
 | 
			
		||||
    val entries: List<QuestEntry>,
 | 
			
		||||
) {
 | 
			
		||||
    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<Uri>,
 | 
			
		||||
    val illustrations: List<Uri>,
 | 
			
		||||
    val background: Uri?,
 | 
			
		||||
    val lastUpdated: Long?,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Lexicon> = parserScope {
 | 
			
		||||
        val ids = hashMapOf<String, Int>()
 | 
			
		||||
        val lexicons = mutableListOf<Lexicon>()
 | 
			
		||||
    fun parse(sheet: ValueRange): List<LexiconDataDbo> = parserScope(timeParser) {
 | 
			
		||||
        val lexicons = mutableListOf<LexiconDataDbo>()
 | 
			
		||||
 | 
			
		||||
        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<LexiconDbo>): List<Lexicon> {
 | 
			
		||||
        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,
 | 
			
		||||
            )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Quest> = parserScope {
 | 
			
		||||
        val entries = hashMapOf<String, MutableList<QuestEntry>>()
 | 
			
		||||
    fun parse(sheet: ValueRange): List<QuestDataDbo> = parserScope(timeParser) {
 | 
			
		||||
        val quests = mutableListOf<QuestDataDbo>()
 | 
			
		||||
 | 
			
		||||
        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<QuestDbo>): List<Quest> {
 | 
			
		||||
        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,
 | 
			
		||||
            )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,12 +7,15 @@ import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure
 | 
			
		|||
import com.pixelized.rplexicon.utilitary.extentions.sheet
 | 
			
		||||
 | 
			
		||||
inline fun <reified T> parserScope(
 | 
			
		||||
    timeParser: TimeUpdateParser? = null,
 | 
			
		||||
    noinline block: SheetParserScope<T>.() -> T
 | 
			
		||||
): T {
 | 
			
		||||
    return SheetParserScope<T>().parse(block)
 | 
			
		||||
    return SheetParserScope<T>(timeParser).parse(block)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SheetParserScope<T> {
 | 
			
		||||
class SheetParserScope<T>(
 | 
			
		||||
    private val timeParser: TimeUpdateParser?,
 | 
			
		||||
) {
 | 
			
		||||
    private var structure: Map<Column, Int> = hashMapOf()
 | 
			
		||||
 | 
			
		||||
    fun updateStructure(row: Any, columns: List<Column>) {
 | 
			
		||||
| 
						 | 
				
			
			@ -81,6 +84,9 @@ class SheetParserScope<T> {
 | 
			
		|||
    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<String> =
 | 
			
		||||
        parse(column)
 | 
			
		||||
            ?.takeIf { it.isNotBlank() }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<List<Lexicon>>(emptyList())
 | 
			
		||||
    val data: StateFlow<List<Lexicon>> 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<Lexicon> = 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,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<List<Quest>>(emptyList())
 | 
			
		||||
    val data: StateFlow<List<Quest>> 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<Quest> = 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,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Boolean>
 | 
			
		||||
 | 
			
		||||
    private val _haveCharacterSheet = mutableStateOf(false)
 | 
			
		||||
    val haveCharacterSheet: State<Boolean> get() = _haveCharacterSheet
 | 
			
		||||
 | 
			
		||||
    private val _character = mutableStateOf<LexiconDetailUio?>(null)
 | 
			
		||||
    val character: State<LexiconDetailUio?> 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
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<LexiconItemU
 | 
			
		|||
            diminutive = "Bru",
 | 
			
		||||
            gender = "Femme",
 | 
			
		||||
            race = "Demi-orc",
 | 
			
		||||
            isNew = true,
 | 
			
		||||
        ),
 | 
			
		||||
        LexiconItemUio.placeholder(),
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,7 +62,8 @@ class LexiconViewModel @Inject constructor(
 | 
			
		|||
                                        diminutive = item.diminutive?.let { "./ $it" },
 | 
			
		||||
                                        gender = item.gender,
 | 
			
		||||
                                        race = item.race,
 | 
			
		||||
                                        isPlayingCharacter = characterSheetRepository.haveSheet(item.name)
 | 
			
		||||
                                        isPlayingCharacter = characterSheetRepository.haveSheet(item.name),
 | 
			
		||||
                                        isNew = item.isNew,
 | 
			
		||||
                                    )
 | 
			
		||||
                                },
 | 
			
		||||
                            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,20 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.screens.quest.detail
 | 
			
		||||
 | 
			
		||||
import android.util.Log
 | 
			
		||||
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.lexicon.LexiconRepository
 | 
			
		||||
import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository
 | 
			
		||||
import com.pixelized.rplexicon.data.repository.lexicon.QuestRepository
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.questDetailArgument
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
| 
						 | 
				
			
			@ -26,29 +31,41 @@ class QuestDetailViewModel @Inject constructor(
 | 
			
		|||
 | 
			
		||||
    init {
 | 
			
		||||
        val argument = savedStateHandle.questDetailArgument
 | 
			
		||||
        val source = questRepository.find(id = argument.id)
 | 
			
		||||
 | 
			
		||||
        if (source != null) {
 | 
			
		||||
            _quest.value = 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.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
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,6 +59,7 @@ class QuestListViewModel @Inject constructor(
 | 
			
		|||
                                        id = item.id,
 | 
			
		||||
                                        title = item.title,
 | 
			
		||||
                                        complete = item.complete,
 | 
			
		||||
                                        isNew = item.isNew,
 | 
			
		||||
                                    )
 | 
			
		||||
                                },
 | 
			
		||||
                            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue