diff --git a/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json b/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json index a770734..ff04a8d 100644 --- a/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json +++ b/app/schemas/com.pixelized.rplexicon.data.database.CompanionDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "c69ae3b018a2383edc96a92797dfdfd3", + "identityHash": "5657267ebe96b4b324d58b58aefce6f8", "entities": [ { "tableName": "lexicon", @@ -104,7 +104,7 @@ }, { "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 NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category` TEXT, `title` TEXT NOT NULL, `subTitle` TEXT, `completed` INTEGER NOT NULL, `questGiverId` TEXT, `questGiverName` TEXT, `locationId` TEXT, `locationName` TEXT, `groupReward` TEXT, `individualReward` TEXT, `description` TEXT NOT NULL, `illustrations` TEXT, `background` TEXT, `lastUpdated` INTEGER, `lastRead` INTEGER NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -137,14 +137,26 @@ "notNull": true }, { - "fieldPath": "questGiver", - "columnName": "questGiver", + "fieldPath": "questGiverId", + "columnName": "questGiverId", "affinity": "TEXT", "notNull": false }, { - "fieldPath": "area", - "columnName": "area", + "fieldPath": "questGiverName", + "columnName": "questGiverName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", "affinity": "TEXT", "notNull": false }, @@ -199,12 +211,119 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `category` TEXT, `description` TEXT, `map` TEXT, `illustrations` TEXT, `lastUpdated` INTEGER, `lastRead` INTEGER NOT NULL, 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": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "map", + "columnName": "map", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "illustrations", + "columnName": "illustrations", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "world", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`parentId` TEXT NOT NULL, `childId` TEXT NOT NULL, `child` TEXT NOT NULL, `x` REAL, `y` REAL, PRIMARY KEY(`parentId`, `childId`))", + "fields": [ + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "childId", + "columnName": "childId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "child", + "columnName": "child", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "x", + "columnName": "x", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "y", + "columnName": "y", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parentId", + "childId" + ] + }, + "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, 'c69ae3b018a2383edc96a92797dfdfd3')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5657267ebe96b4b324d58b58aefce6f8')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/business/SearchUseCase.kt b/app/src/main/java/com/pixelized/rplexicon/business/SearchUseCase.kt index 67692e1..b5859ef 100644 --- a/app/src/main/java/com/pixelized/rplexicon/business/SearchUseCase.kt +++ b/app/src/main/java/com/pixelized/rplexicon/business/SearchUseCase.kt @@ -155,8 +155,8 @@ class SearchUseCase @Inject constructor( val entry = item.entries.any { val title = it.subtitle?.contains(criteria, true) == true val subTitle = it.subtitle?.contains(criteria, true) == true - val questGiver = it.questGiver?.contains(criteria, true) == true - val location = it.area?.contains(criteria, true) == true + val questGiver = it.questGiverName?.contains(criteria, true) == true + val location = it.locationName?.contains(criteria, true) == true val individualReward = it.individualReward?.contains(criteria, true) == true val groupReward = it.groupReward?.contains(criteria, true) == true val description = it.description.contains(criteria, true) @@ -169,8 +169,8 @@ class SearchUseCase @Inject constructor( criterion.map { criteria -> val title = it.subtitle?.contains(criteria, true) == true val subTitle = it.subtitle?.contains(criteria, true) == true - val questGiver = it.questGiver?.contains(criteria, true) == true - val location = it.area?.contains(criteria, true) == true + val questGiver = it.questGiverName?.contains(criteria, true) == true + val location = it.locationName?.contains(criteria, true) == true val individualReward = it.individualReward?.contains(criteria, true) == true val groupReward = it.groupReward?.contains(criteria, true) == true val description = it.description.contains(criteria, true) @@ -188,7 +188,7 @@ class SearchUseCase @Inject constructor( highlightRegex styleWith typography.search.titleHighlight, dropCapRegex styleWith typography.titleMediumDropCap, ), - owner = entry?.questGiver?.let { + owner = entry?.questGiverName?.let { AnnotatedString( text = "$ownerPrefix ", spanStyle = typography.search.extractBold, @@ -197,7 +197,7 @@ class SearchUseCase @Inject constructor( highlightRegex styleWith typography.search.extractHighlight, ) }, - location = entry?.area?.let { + location = entry?.locationName?.let { AnnotatedString( text = "$locationPrefix ", spanStyle = typography.search.extractBold, @@ -256,7 +256,7 @@ class SearchUseCase @Inject constructor( val category = item.category?.contains(criteria, true) == true val name = item.name.contains(criteria, true) val description = item.description?.contains(criteria, true) == true - val child = item.child.any { it.second.name.contains(criteria, true) } + val child = item.child.any { it.name.contains(criteria, true) } category || name || description || child }.all { it } }.map { item -> @@ -281,7 +281,7 @@ class SearchUseCase @Inject constructor( ) }, destination = item.child.mapNotNull { child -> - highlightRegex.find(child.second.name)?.let { child.second.name } + highlightRegex.find(child.name)?.let { child.name } }.takeIf { it.any() }?.let { AnnotatedString( text = "$destinationPrefix ", 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 index 330f776..2f6cafe 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/database/CompanionDatabase.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/CompanionDatabase.kt @@ -1,12 +1,15 @@ package com.pixelized.rplexicon.data.database import android.content.Context -import androidx.room.AutoMigration 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.location.LocationDao +import com.pixelized.rplexicon.data.database.location.LocationDbo +import com.pixelized.rplexicon.data.database.location.WorldDao +import com.pixelized.rplexicon.data.database.location.WorldDbo import com.pixelized.rplexicon.data.database.quest.QuestDao import com.pixelized.rplexicon.data.database.quest.QuestDbo import dagger.Module @@ -16,19 +19,20 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent @Database( - entities = [LexiconDbo::class, QuestDbo::class], + entities = [LexiconDbo::class, QuestDbo::class, LocationDbo::class, WorldDbo::class], version = 1, exportSchema = true, ) abstract class CompanionDatabase : RoomDatabase() { abstract fun lexiconDao(): LexiconDao abstract fun questsDao(): QuestDao + abstract fun locationDao(): LocationDao + abstract fun worldDao(): WorldDao } @Module @InstallIn(SingletonComponent::class) class DatabaseModule { - @Provides fun provideCompanionDatabase( @ApplicationContext context: Context, 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 index 8d8aba6..f233f38 100644 --- 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 @@ -16,9 +16,6 @@ interface LexiconDao { @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(onConflict = OnConflictStrategy.IGNORE) fun insert(item: LexiconDbo) diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/location/LocationDao.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/location/LocationDao.kt new file mode 100644 index 0000000..de7dd0b --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/location/LocationDao.kt @@ -0,0 +1,26 @@ +package com.pixelized.rplexicon.data.database.location + +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 LocationDao { + @Query("SELECT * from location") + fun getAllFlow(): Flow> + + @Query("SELECT * from location WHERE id = :id") + fun getByIdFlow(id: String): Flow + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(item: LocationDbo) + + @Update(entity = LocationDbo::class) + fun update(item: LocationDataDbo): Int + + @Update(entity = LocationDbo::class) + suspend fun update(item: LocationReadTimestampDbo): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/location/LocationDbo.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/location/LocationDbo.kt new file mode 100644 index 0000000..201877f --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/location/LocationDbo.kt @@ -0,0 +1,47 @@ +package com.pixelized.rplexicon.data.database.location + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "location") +data class LocationDbo( + @PrimaryKey + val id: String, + val name: String, + val category: String?, + val description: String?, + val map: String?, + val illustrations: String?, + val lastUpdated: Long?, + val lastRead: Long, +) + +@Entity(tableName = "location") +data class LocationDataDbo( + @PrimaryKey + val id: String, + val name: String, + val category: String?, + val description: String?, + val map: String?, + val illustrations: String?, + val lastUpdated: Long?, +) { + infix fun with(lastRead: Long) = LocationDbo( + id = id, + name = name, + category = category, + description = description, + map = map, + illustrations = illustrations, + lastUpdated = lastUpdated, + lastRead = lastRead, + ) +} + +@Entity(tableName = "location") +data class LocationReadTimestampDbo( + @PrimaryKey + val id: String, + val lastRead: Long, +) diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/location/WorldDao.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/location/WorldDao.kt new file mode 100644 index 0000000..d3c818f --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/location/WorldDao.kt @@ -0,0 +1,23 @@ +package com.pixelized.rplexicon.data.database.location + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface WorldDao { + + @Query("SELECT * from world") + fun getAllFlow(): Flow> + + @Query("SELECT * from world WHERE parentId = :parentId") + fun getByParentIdFlow(parentId: String): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(items: List) + + @Query("DELETE FROM world") + fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/location/WorldDbo.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/location/WorldDbo.kt new file mode 100644 index 0000000..f152919 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/location/WorldDbo.kt @@ -0,0 +1,12 @@ +package com.pixelized.rplexicon.data.database.location + +import androidx.room.Entity + +@Entity(tableName = "world", primaryKeys = ["parentId", "childId"]) +data class WorldDbo( + val parentId: String, + val childId: String, + val child: String, + val x: Float?, + val y: Float?, +) 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 index ea47af6..533d7ac 100644 --- 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 @@ -11,8 +11,10 @@ data class QuestDbo( val title: String, val subTitle: String?, val completed: Boolean, - val questGiver: String?, - val area: String?, + val questGiverId: String?, + val questGiverName: String?, + val locationId: String?, + val locationName: String?, val groupReward: String?, val individualReward: String?, val description: String, @@ -30,8 +32,10 @@ data class QuestDataDbo( val title: String, val subTitle: String?, val completed: Boolean, - val questGiver: String?, - val area: String?, + val questGiverId: String?, + val questGiverName: String?, + val locationId: String?, + val locationName: String?, val groupReward: String?, val individualReward: String?, val description: String, @@ -45,8 +49,10 @@ data class QuestDataDbo( title = title, subTitle = subTitle, completed = completed, - questGiver = questGiver, - area = area, + questGiverId = questGiverId, + questGiverName = questGiverName, + locationId = locationId, + locationName = locationName, groupReward = groupReward, individualReward = individualReward, description = description, diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/Location.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/Location.kt index ff94f1c..824db5c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/model/Location.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/Location.kt @@ -7,8 +7,18 @@ data class Location( val id: String, val name: String, val category: String?, - val uri: Uri?, val description: String?, + val map: Uri?, val illustrations: List, - val child: List> = emptyList(), -) \ No newline at end of file + val lastUpdated: Long?, + val lastRead: Long, + val child: List = emptyList(), +) { + val isNew: Boolean get() = lastRead - (lastUpdated ?: 0) < 0 + + data class Child( + val id: String, + val name: String, + val position: Offset, + ) +} \ 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 65e1408..9f3de49 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 @@ -22,8 +22,10 @@ data class QuestEntry( val title: String, val subtitle: String?, val complete: Boolean, - val questGiver: String?, - val area: String?, + val questGiverId: String?, + val questGiverName: String?, + val locationId: String?, + val locationName: String?, val groupReward: String?, val individualReward: String?, val description: String, 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 ed71ea7..624bf9d 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,7 +2,6 @@ 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 @@ -32,8 +31,10 @@ class QuestParser @Inject constructor( title = quest, subTitle = item.parse(column = SUB_TITLE), completed = item.parseBool(column = COMPLETED) ?: false, - questGiver = item.parse(column = QUEST_GIVER), - area = item.parse(column = AREA), + questGiverId = item.parse(column = QUEST_GIVER_ID), + questGiverName = item.parse(column = QUEST_GIVER_NAME), + locationId = item.parse(column = LOCATION_ID), + locationName = item.parse(column = LOCATION_NAME), groupReward = item.parse(column = GROUP_REWARD), individualReward = item.parse(column = INDIVIDUAL_REWARD), description = description, @@ -71,8 +72,10 @@ class QuestParser @Inject constructor( title = data.title, subtitle = data.subTitle, complete = data.completed, - questGiver = data.questGiver, - area = data.area, + questGiverId = data.questGiverId, + questGiverName = data.questGiverName, + locationId = data.locationId, + locationName = data.locationName, groupReward = data.groupReward, individualReward = data.individualReward, description = data.description, @@ -88,8 +91,10 @@ class QuestParser @Inject constructor( private val CATEGORY = column("Catégorie") private val SUB_TITLE = column("Sous Titre") private val COMPLETED = column("Compléter") - private val QUEST_GIVER = column("Commanditaire") - private val AREA = column("Lieu") + private val QUEST_GIVER_ID = column("Id Commanditaire") + private val QUEST_GIVER_NAME = column("Commanditaire") + private val LOCATION_ID = column("Id Lieu") + private val LOCATION_NAME = column("Lieu") private val GROUP_REWARD = column("Récompense de groupe") private val INDIVIDUAL_REWARD = column("Récompense individuelle") private val DESCRIPTION = column("Description") @@ -104,8 +109,10 @@ class QuestParser @Inject constructor( CATEGORY, SUB_TITLE, COMPLETED, - QUEST_GIVER, - AREA, + QUEST_GIVER_ID, + QUEST_GIVER_NAME, + LOCATION_ID, + LOCATION_NAME, GROUP_REWARD, INDIVIDUAL_REWARD, DESCRIPTION, diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/map/LocationParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/map/LocationParser.kt index b733d91..a91496c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/map/LocationParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/map/LocationParser.kt @@ -1,40 +1,42 @@ package com.pixelized.rplexicon.data.parser.map -import com.google.api.services.sheets.v4.model.ValueRange +import androidx.compose.ui.geometry.Offset +import com.pixelized.rplexicon.data.database.location.LocationDbo +import com.pixelized.rplexicon.data.database.location.WorldDbo import com.pixelized.rplexicon.data.model.Location -import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure +import com.pixelized.rplexicon.data.parser.IllustrationParser +import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull import javax.inject.Inject class LocationParser @Inject constructor( - private val mapParser: MapParser, - private val worldParser: WorldParser, + private val illustrationParser: IllustrationParser, ) { - @Throws(IncompatibleSheetStructure::class) - fun parse(mapSheet: ValueRange, worldSheet: ValueRange): List { - val localMaps = mapParser.parse(sheet = mapSheet) - val localWorld = worldParser.parse(sheet = worldSheet) - - val mapHash = localMaps - .map { localMap -> - Location( - id = localMap.name, - name = localMap.name, - category = localMap.category, - uri = localMap.uri, - description = localMap.description, - illustrations = localMap.illustrations, - child = emptyList(), + fun convert(location: LocationDbo, world: Map>): Location { + val map = Location( + id = location.id, + name = location.name, + category = location.category, + description = location.description, + map = location.map?.toUriOrNull(), + illustrations = illustrationParser.parse(value = location.illustrations), + lastUpdated = location.lastUpdated, + lastRead = location.lastRead, + child = world[location.id]?.map { + Location.Child( + id = it.childId, + name = it.child, + position = when { + it.x != null && it.y != null -> Offset(it.x, it.y) + else -> Offset.Unspecified + } ) - }.associateBy { it.name } + } ?: emptyList() + ) + return map + } - val maps = mapHash.map { entry -> - entry.value.copy( - child = localWorld - .filter { it.parent == entry.key } - .mapNotNull { world -> mapHash[world.child]?.let { map -> world.position to map } } - ) - } - - return maps + fun convert(locations: List, world: List): List { + val worldHash = world.groupBy { it.parentId } + return locations.map { convert(location = it, world = worldHash) } } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/map/MapParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/map/MapParser.kt index bdda382..9c2f2fe 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/map/MapParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/map/MapParser.kt @@ -1,30 +1,32 @@ package com.pixelized.rplexicon.data.parser.map -import android.net.Uri import com.google.api.services.sheets.v4.model.ValueRange -import com.pixelized.rplexicon.data.parser.IllustrationParser +import com.pixelized.rplexicon.data.database.location.LocationDataDbo +import com.pixelized.rplexicon.data.parser.TimeUpdateParser import com.pixelized.rplexicon.data.parser.column import com.pixelized.rplexicon.data.parser.parserScope import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import javax.inject.Inject class MapParser @Inject constructor( - private val illustrationParser: IllustrationParser, + private val timeParser: TimeUpdateParser, ) { - @Throws(IncompatibleSheetStructure::class) - fun parse(sheet: ValueRange): List = parserScope { - val maps = mutableListOf() + fun parse(sheet: ValueRange): List = parserScope(timeParser) { + val maps = mutableListOf() sheet.forEachDataLine(columns = COLUMNS) { + val id = it.parse(column = ID) val name = it.parse(column = NAME) - if (name != null) { - val map = MapDto( + if (id != null && name != null) { + val map = LocationDataDbo( + id = id, name = name, category = it.parse(column = CATEGORY), - uri = it.parseUri(column = URI), description = it.parse(column = DESCRIPTION), - illustrations = illustrationParser.parse(it.parse(column = ILLUSTRATIONS)), + map = it.parse(column = MAP), + illustrations = it.parse(column = ILLUSTRATIONS), + lastUpdated = it.parseTime(column = UPDATE), ) maps.add(map) } @@ -33,20 +35,16 @@ class MapParser @Inject constructor( return@parserScope maps } - data class MapDto( - val name: String, - val category: String?, - val uri: Uri?, - val description: String?, - val illustrations: List, - ) - companion object { + private val ID = column("Id") private val NAME = column("Nom") private val CATEGORY = column("Catégorie") - private val URI = column("Carte") + private val MAP = column("Carte") private val DESCRIPTION = column("Description") private val ILLUSTRATIONS = column("Illustrations") - private val COLUMNS get() = listOf(NAME, CATEGORY, URI, DESCRIPTION, ILLUSTRATIONS) + private val UPDATE = column("Mise à jour") + + private val COLUMNS + get() = listOf(ID, NAME, CATEGORY, MAP, DESCRIPTION, ILLUSTRATIONS, UPDATE) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/map/WorldParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/map/WorldParser.kt index caf905d..cba43ae 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/map/WorldParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/map/WorldParser.kt @@ -1,7 +1,7 @@ package com.pixelized.rplexicon.data.parser.map -import androidx.compose.ui.geometry.Offset import com.google.api.services.sheets.v4.model.ValueRange +import com.pixelized.rplexicon.data.database.location.WorldDbo import com.pixelized.rplexicon.data.parser.column import com.pixelized.rplexicon.data.parser.parserScope import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure @@ -10,22 +10,28 @@ import javax.inject.Inject class WorldParser @Inject constructor() { @Throws(IncompatibleSheetStructure::class) - fun parse(sheet: ValueRange): List = parserScope { - val worlds = mutableListOf() + fun parse(sheet: ValueRange): List = parserScope { + val worlds = mutableListOf() sheet.forEachDataLine(columns = COLUMNS) { line -> + val parentId = line.parse(column = PARENT_ID) val parent = line.parse(column = PARENT) + val childId = line.parse(column = CHILD_ID) val child = line.parse(column = CHILD) val x = line.parseFloat(column = X) val y = line.parseFloat(column = Y) - if (child != null) { - val world = WorldDto( - parent = parent, + + // We check but don't use the parent value, because of the pre validation of the datasheet + if ( + parentId != null && parent != null && + childId != null && child != null + ) { + val world = WorldDbo( + parentId = parentId, + childId = childId, child = child, - position = when { - x != null && y != null -> Offset(x = x, y = y) - else -> Offset.Unspecified - }, + x = x, + y = y, ) worlds.add(world) } @@ -34,17 +40,13 @@ class WorldParser @Inject constructor() { return@parserScope worlds } - data class WorldDto( - val parent: String?, - val child: String, - val position: Offset, - ) - companion object { + private val PARENT_ID = column("Id Parent") private val PARENT = column("Parent") + private val CHILD_ID = column("Id Enfant") private val CHILD = column("Enfant") private val X = column("X") private val Y = column("Y") - private val COLUMNS get() = listOf(PARENT, CHILD, X, Y) + private val COLUMNS get() = listOf(PARENT_ID, PARENT, CHILD_ID, CHILD, X, Y) } } \ 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 4412c3c..9195968 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 @@ -54,15 +54,6 @@ class LexiconRepository @Inject constructor( } } - /** - * 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. diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LocationRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LocationRepository.kt index 930c842..39f768f 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LocationRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/lexicon/LocationRepository.kt @@ -1,31 +1,67 @@ package com.pixelized.rplexicon.data.repository.lexicon +import com.pixelized.rplexicon.data.database.CompanionDatabase +import com.pixelized.rplexicon.data.database.location.LocationReadTimestampDbo import com.pixelized.rplexicon.data.model.Location import com.pixelized.rplexicon.data.parser.map.LocationParser +import com.pixelized.rplexicon.data.parser.map.MapParser +import com.pixelized.rplexicon.data.parser.map.WorldParser 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.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @Singleton class LocationRepository @Inject constructor( private val googleRepository: GoogleSheetServiceRepository, - private val parser: LocationParser, + private val database: CompanionDatabase, + private val locationParser: LocationParser, + private val mapParser: MapParser, + private val worldParser: WorldParser, ) { + 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?): Location? { - return id?.let { _data.value.firstOrNull { it.id == id } } + init { + scope.launch(Dispatchers.IO) { + database.locationDao().getAllFlow() + .combine(database.worldDao().getAllFlow()) { location, world -> location to world } + .collect { data -> + val (locations, world) = data + _data.value = locationParser.convert(locations = locations, world = world) + } + } + } + + fun getByIdFlow(id: String?): Flow = when (id) { + null -> emptyFlow() + else -> database.locationDao().getByIdFlow(id = id) + .combine(database.worldDao().getByParentIdFlow(parentId = id)) { map, link -> + map to link.groupBy { it.parentId } + } + .map { data -> + val (locations, world) = data + locationParser.convert(location = locations, world = world) + } } @Throws(IncompatibleSheetStructure::class, Exception::class) @@ -35,12 +71,36 @@ class LocationRepository @Inject constructor( async { sheets.get(LexiconBinder.ID, LexiconBinder.MAP).execute() }, async { sheets.get(LexiconBinder.ID, LexiconBinder.WORLD).execute() }, ) - val data = parser.parse(mapSheet = map, worldSheet = world) - _data.emit(data) + database.runInTransaction { + val mapDao = database.locationDao() + mapParser.parse(map).forEach { item -> + val row = mapDao.update(item) + if (row == 0) mapDao.insert(item with System.currentTimeMillis()) + } + + val worldDao = database.worldDao() + worldDao.deleteAll() + worldDao.insert(items = worldParser.parse(world)) + } + lastSuccessFullUpdate = Update.currentTime() } } + /** + * Update the [Location#lastTime] field of a [Location] instance. + * @param id the id of the [Location] instance. + * @param timestamp the timestamp that will update the lastRead filed. + */ + suspend fun updateReadTime(id: String, timestamp: Long = System.currentTimeMillis()) { + database.locationDao().update( + item = LocationReadTimestampDbo( + id = id, + lastRead = timestamp, + ) + ) + } + companion object { const val WORLD_SHEET_URL = "https://docs.google.com/spreadsheets/d/${LexiconBinder.ID}/edit#gid=1943877267" diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt index 4bbbece..70bab15 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.locationDetailArgument @@ -16,6 +17,9 @@ import com.pixelized.rplexicon.utilitary.cells import com.pixelized.rplexicon.utilitary.line import com.pixelized.rplexicon.utilitary.table import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject import kotlin.math.max @@ -28,7 +32,9 @@ class LocationDetailViewModel @Inject constructor( private val clipboard = application.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val sheetUri = LocationRepository.WORLD_SHEET_URL - val location: State + + private val _location = mutableStateOf(null) + val location: State get() = _location private val _selectedMarquee = mutableStateOf(null) val selectedMarquee: State get() = _selectedMarquee @@ -44,25 +50,35 @@ class LocationDetailViewModel @Inject constructor( init { val argument = savedStateHandle.locationDetailArgument - val source = repository.find(id = argument.id) - location = mutableStateOf( - source?.let { - LocationDetailUio( - name = it.name, - map = it.uri, - description = it.description, - illustrations = it.illustrations, - marquees = it.child.map { child -> - MarqueeUio( - id = child.second.id, - name = child.second.name, - position = child.first, - ) - } - ) + viewModelScope.launch { + launch(Dispatchers.IO) { + // update the last read time for that lexicon + repository.updateReadTime(id = argument.id) } - ) + launch(Dispatchers.IO) { + // fetch and display the detail data. + repository.getByIdFlow(id = argument.id).collect { source -> + val location = LocationDetailUio( + name = source.name, + map = source.map, + description = source.description, + illustrations = source.illustrations, + marquees = source.child.map { child -> + MarqueeUio( + id = child.id, + name = child.name, + position = child.position, + ) + } + ) + // Update the UI state + withContext(Dispatchers.Main) { + _location.value = location + } + } + } + } } fun onSelectMarquee(marqueeUio: MarqueeUio) { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationItem.kt index 6977f39..dcd172a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationItem.kt @@ -38,6 +38,7 @@ data class LocationItemUio( val id: String, val title: String, val placeholder: Boolean = false, + val isNew: Boolean = false, ) { companion object { fun preview( @@ -73,6 +74,7 @@ fun LocationItem( true -> Modifier.placeholder { true } else -> Modifier.alignByBaseline() }, + color = if (item.isNew) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, style = typography.base.titleMedium, text = LOS_FULL, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationViewModel.kt index e7afd5b..6c9b3cf 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationViewModel.kt @@ -55,8 +55,9 @@ class LocationViewModel @Inject constructor( }, valueTransform = { entry -> LocationItemUio( - id = entry.name, + id = entry.id, title = entry.name, + isNew = entry.isNew, ) } ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt index 4aa307f..4d89b5e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt @@ -1,13 +1,10 @@ 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 @@ -21,8 +18,6 @@ import javax.inject.Inject class QuestDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, questRepository: QuestRepository, - lexiconRepository: LexiconRepository, - locationRepository: LocationRepository, ) : ViewModel() { private val _quest = mutableStateOf(null) val quest: State get() = _quest @@ -46,13 +41,12 @@ class QuestDetailViewModel @Inject constructor( 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, + giverId = entry.questGiverId, + giver = entry.questGiverName, + placeId = entry.locationId, + place = entry.locationName, globalReward = entry.groupReward, individualReward = entry.individualReward, description = entry.description,