Refactor quest id reference & locatino isNew feature.

This commit is contained in:
Thomas Andres Gomez 2024-01-10 21:44:29 +01:00
parent 9f44ce4543
commit 3904ab22ff
21 changed files with 469 additions and 150 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "c69ae3b018a2383edc96a92797dfdfd3", "identityHash": "5657267ebe96b4b324d58b58aefce6f8",
"entities": [ "entities": [
{ {
"tableName": "lexicon", "tableName": "lexicon",
@ -104,7 +104,7 @@
}, },
{ {
"tableName": "quest", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -137,14 +137,26 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "questGiver", "fieldPath": "questGiverId",
"columnName": "questGiver", "columnName": "questGiverId",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, },
{ {
"fieldPath": "area", "fieldPath": "questGiverName",
"columnName": "area", "columnName": "questGiverName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "locationId",
"columnName": "locationId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "locationName",
"columnName": "locationName",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, },
@ -199,12 +211,119 @@
}, },
"indices": [], "indices": [],
"foreignKeys": [] "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": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }

View file

@ -155,8 +155,8 @@ class SearchUseCase @Inject constructor(
val entry = item.entries.any { val entry = item.entries.any {
val title = it.subtitle?.contains(criteria, true) == true val title = it.subtitle?.contains(criteria, true) == true
val subTitle = it.subtitle?.contains(criteria, true) == true val subTitle = it.subtitle?.contains(criteria, true) == true
val questGiver = it.questGiver?.contains(criteria, true) == true val questGiver = it.questGiverName?.contains(criteria, true) == true
val location = it.area?.contains(criteria, true) == true val location = it.locationName?.contains(criteria, true) == true
val individualReward = it.individualReward?.contains(criteria, true) == true val individualReward = it.individualReward?.contains(criteria, true) == true
val groupReward = it.groupReward?.contains(criteria, true) == true val groupReward = it.groupReward?.contains(criteria, true) == true
val description = it.description.contains(criteria, true) val description = it.description.contains(criteria, true)
@ -169,8 +169,8 @@ class SearchUseCase @Inject constructor(
criterion.map { criteria -> criterion.map { criteria ->
val title = it.subtitle?.contains(criteria, true) == true val title = it.subtitle?.contains(criteria, true) == true
val subTitle = it.subtitle?.contains(criteria, true) == true val subTitle = it.subtitle?.contains(criteria, true) == true
val questGiver = it.questGiver?.contains(criteria, true) == true val questGiver = it.questGiverName?.contains(criteria, true) == true
val location = it.area?.contains(criteria, true) == true val location = it.locationName?.contains(criteria, true) == true
val individualReward = it.individualReward?.contains(criteria, true) == true val individualReward = it.individualReward?.contains(criteria, true) == true
val groupReward = it.groupReward?.contains(criteria, true) == true val groupReward = it.groupReward?.contains(criteria, true) == true
val description = it.description.contains(criteria, true) val description = it.description.contains(criteria, true)
@ -188,7 +188,7 @@ class SearchUseCase @Inject constructor(
highlightRegex styleWith typography.search.titleHighlight, highlightRegex styleWith typography.search.titleHighlight,
dropCapRegex styleWith typography.titleMediumDropCap, dropCapRegex styleWith typography.titleMediumDropCap,
), ),
owner = entry?.questGiver?.let { owner = entry?.questGiverName?.let {
AnnotatedString( AnnotatedString(
text = "$ownerPrefix ", text = "$ownerPrefix ",
spanStyle = typography.search.extractBold, spanStyle = typography.search.extractBold,
@ -197,7 +197,7 @@ class SearchUseCase @Inject constructor(
highlightRegex styleWith typography.search.extractHighlight, highlightRegex styleWith typography.search.extractHighlight,
) )
}, },
location = entry?.area?.let { location = entry?.locationName?.let {
AnnotatedString( AnnotatedString(
text = "$locationPrefix ", text = "$locationPrefix ",
spanStyle = typography.search.extractBold, spanStyle = typography.search.extractBold,
@ -256,7 +256,7 @@ class SearchUseCase @Inject constructor(
val category = item.category?.contains(criteria, true) == true val category = item.category?.contains(criteria, true) == true
val name = item.name.contains(criteria, true) val name = item.name.contains(criteria, true)
val description = item.description?.contains(criteria, true) == 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 category || name || description || child
}.all { it } }.all { it }
}.map { item -> }.map { item ->
@ -281,7 +281,7 @@ class SearchUseCase @Inject constructor(
) )
}, },
destination = item.child.mapNotNull { child -> 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 { }.takeIf { it.any() }?.let {
AnnotatedString( AnnotatedString(
text = "$destinationPrefix ", text = "$destinationPrefix ",

View file

@ -1,12 +1,15 @@
package com.pixelized.rplexicon.data.database package com.pixelized.rplexicon.data.database
import android.content.Context import android.content.Context
import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.pixelized.rplexicon.data.database.lexicon.LexiconDao import com.pixelized.rplexicon.data.database.lexicon.LexiconDao
import com.pixelized.rplexicon.data.database.lexicon.LexiconDbo 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.QuestDao
import com.pixelized.rplexicon.data.database.quest.QuestDbo import com.pixelized.rplexicon.data.database.quest.QuestDbo
import dagger.Module import dagger.Module
@ -16,19 +19,20 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
@Database( @Database(
entities = [LexiconDbo::class, QuestDbo::class], entities = [LexiconDbo::class, QuestDbo::class, LocationDbo::class, WorldDbo::class],
version = 1, version = 1,
exportSchema = true, exportSchema = true,
) )
abstract class CompanionDatabase : RoomDatabase() { abstract class CompanionDatabase : RoomDatabase() {
abstract fun lexiconDao(): LexiconDao abstract fun lexiconDao(): LexiconDao
abstract fun questsDao(): QuestDao abstract fun questsDao(): QuestDao
abstract fun locationDao(): LocationDao
abstract fun worldDao(): WorldDao
} }
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class DatabaseModule { class DatabaseModule {
@Provides @Provides
fun provideCompanionDatabase( fun provideCompanionDatabase(
@ApplicationContext context: Context, @ApplicationContext context: Context,

View file

@ -16,9 +16,6 @@ interface LexiconDao {
@Query("SELECT * from lexicon WHERE id = :id") @Query("SELECT * from lexicon WHERE id = :id")
fun getByIdFlow(id: String): Flow<LexiconDbo> fun getByIdFlow(id: String): Flow<LexiconDbo>
@Query("SELECT id from lexicon WHERE name = :name LIMIT 1")
suspend fun getIdByName(name: String): String?
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(item: LexiconDbo) fun insert(item: LexiconDbo)

View file

@ -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<List<LocationDbo>>
@Query("SELECT * from location WHERE id = :id")
fun getByIdFlow(id: String): Flow<LocationDbo>
@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
}

View file

@ -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,
)

View file

@ -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<List<WorldDbo>>
@Query("SELECT * from world WHERE parentId = :parentId")
fun getByParentIdFlow(parentId: String): Flow<List<WorldDbo>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(items: List<WorldDbo>)
@Query("DELETE FROM world")
fun deleteAll()
}

View file

@ -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?,
)

View file

@ -11,8 +11,10 @@ data class QuestDbo(
val title: String, val title: String,
val subTitle: String?, val subTitle: String?,
val completed: Boolean, val completed: Boolean,
val questGiver: String?, val questGiverId: String?,
val area: String?, val questGiverName: String?,
val locationId: String?,
val locationName: String?,
val groupReward: String?, val groupReward: String?,
val individualReward: String?, val individualReward: String?,
val description: String, val description: String,
@ -30,8 +32,10 @@ data class QuestDataDbo(
val title: String, val title: String,
val subTitle: String?, val subTitle: String?,
val completed: Boolean, val completed: Boolean,
val questGiver: String?, val questGiverId: String?,
val area: String?, val questGiverName: String?,
val locationId: String?,
val locationName: String?,
val groupReward: String?, val groupReward: String?,
val individualReward: String?, val individualReward: String?,
val description: String, val description: String,
@ -45,8 +49,10 @@ data class QuestDataDbo(
title = title, title = title,
subTitle = subTitle, subTitle = subTitle,
completed = completed, completed = completed,
questGiver = questGiver, questGiverId = questGiverId,
area = area, questGiverName = questGiverName,
locationId = locationId,
locationName = locationName,
groupReward = groupReward, groupReward = groupReward,
individualReward = individualReward, individualReward = individualReward,
description = description, description = description,

View file

@ -7,8 +7,18 @@ data class Location(
val id: String, val id: String,
val name: String, val name: String,
val category: String?, val category: String?,
val uri: Uri?,
val description: String?, val description: String?,
val map: Uri?,
val illustrations: List<Uri>, val illustrations: List<Uri>,
val child: List<Pair<Offset, Location>> = emptyList(), val lastUpdated: Long?,
) val lastRead: Long,
val child: List<Child> = emptyList(),
) {
val isNew: Boolean get() = lastRead - (lastUpdated ?: 0) < 0
data class Child(
val id: String,
val name: String,
val position: Offset,
)
}

View file

@ -22,8 +22,10 @@ data class QuestEntry(
val title: String, val title: String,
val subtitle: String?, val subtitle: String?,
val complete: Boolean, val complete: Boolean,
val questGiver: String?, val questGiverId: String?,
val area: String?, val questGiverName: String?,
val locationId: String?,
val locationName: String?,
val groupReward: String?, val groupReward: String?,
val individualReward: String?, val individualReward: String?,
val description: String, val description: String,

View file

@ -2,7 +2,6 @@ package com.pixelized.rplexicon.data.parser
import com.google.api.services.sheets.v4.model.ValueRange 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.QuestDataDbo
import com.pixelized.rplexicon.data.database.quest.QuestDbo import com.pixelized.rplexicon.data.database.quest.QuestDbo
import com.pixelized.rplexicon.data.model.Quest import com.pixelized.rplexicon.data.model.Quest
@ -32,8 +31,10 @@ class QuestParser @Inject constructor(
title = quest, title = quest,
subTitle = item.parse(column = SUB_TITLE), subTitle = item.parse(column = SUB_TITLE),
completed = item.parseBool(column = COMPLETED) ?: false, completed = item.parseBool(column = COMPLETED) ?: false,
questGiver = item.parse(column = QUEST_GIVER), questGiverId = item.parse(column = QUEST_GIVER_ID),
area = item.parse(column = AREA), 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), groupReward = item.parse(column = GROUP_REWARD),
individualReward = item.parse(column = INDIVIDUAL_REWARD), individualReward = item.parse(column = INDIVIDUAL_REWARD),
description = description, description = description,
@ -71,8 +72,10 @@ class QuestParser @Inject constructor(
title = data.title, title = data.title,
subtitle = data.subTitle, subtitle = data.subTitle,
complete = data.completed, complete = data.completed,
questGiver = data.questGiver, questGiverId = data.questGiverId,
area = data.area, questGiverName = data.questGiverName,
locationId = data.locationId,
locationName = data.locationName,
groupReward = data.groupReward, groupReward = data.groupReward,
individualReward = data.individualReward, individualReward = data.individualReward,
description = data.description, description = data.description,
@ -88,8 +91,10 @@ class QuestParser @Inject constructor(
private val CATEGORY = column("Catégorie") private val CATEGORY = column("Catégorie")
private val SUB_TITLE = column("Sous Titre") private val SUB_TITLE = column("Sous Titre")
private val COMPLETED = column("Compléter") private val COMPLETED = column("Compléter")
private val QUEST_GIVER = column("Commanditaire") private val QUEST_GIVER_ID = column("Id Commanditaire")
private val AREA = column("Lieu") 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 GROUP_REWARD = column("Récompense de groupe")
private val INDIVIDUAL_REWARD = column("Récompense individuelle") private val INDIVIDUAL_REWARD = column("Récompense individuelle")
private val DESCRIPTION = column("Description") private val DESCRIPTION = column("Description")
@ -104,8 +109,10 @@ class QuestParser @Inject constructor(
CATEGORY, CATEGORY,
SUB_TITLE, SUB_TITLE,
COMPLETED, COMPLETED,
QUEST_GIVER, QUEST_GIVER_ID,
AREA, QUEST_GIVER_NAME,
LOCATION_ID,
LOCATION_NAME,
GROUP_REWARD, GROUP_REWARD,
INDIVIDUAL_REWARD, INDIVIDUAL_REWARD,
DESCRIPTION, DESCRIPTION,

View file

@ -1,40 +1,42 @@
package com.pixelized.rplexicon.data.parser.map 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.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 import javax.inject.Inject
class LocationParser @Inject constructor( class LocationParser @Inject constructor(
private val mapParser: MapParser, private val illustrationParser: IllustrationParser,
private val worldParser: WorldParser,
) { ) {
@Throws(IncompatibleSheetStructure::class) fun convert(location: LocationDbo, world: Map<String, List<WorldDbo>>): Location {
fun parse(mapSheet: ValueRange, worldSheet: ValueRange): List<Location> { val map = Location(
val localMaps = mapParser.parse(sheet = mapSheet) id = location.id,
val localWorld = worldParser.parse(sheet = worldSheet) name = location.name,
category = location.category,
val mapHash = localMaps description = location.description,
.map { localMap -> map = location.map?.toUriOrNull(),
Location( illustrations = illustrationParser.parse(value = location.illustrations),
id = localMap.name, lastUpdated = location.lastUpdated,
name = localMap.name, lastRead = location.lastRead,
category = localMap.category, child = world[location.id]?.map {
uri = localMap.uri, Location.Child(
description = localMap.description, id = it.childId,
illustrations = localMap.illustrations, name = it.child,
child = emptyList(), 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 -> fun convert(locations: List<LocationDbo>, world: List<WorldDbo>): List<Location> {
entry.value.copy( val worldHash = world.groupBy { it.parentId }
child = localWorld return locations.map { convert(location = it, world = worldHash) }
.filter { it.parent == entry.key }
.mapNotNull { world -> mapHash[world.child]?.let { map -> world.position to map } }
)
}
return maps
} }
} }

View file

@ -1,30 +1,32 @@
package com.pixelized.rplexicon.data.parser.map package com.pixelized.rplexicon.data.parser.map
import android.net.Uri
import com.google.api.services.sheets.v4.model.ValueRange 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.column
import com.pixelized.rplexicon.data.parser.parserScope import com.pixelized.rplexicon.data.parser.parserScope
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import javax.inject.Inject import javax.inject.Inject
class MapParser @Inject constructor( class MapParser @Inject constructor(
private val illustrationParser: IllustrationParser, private val timeParser: TimeUpdateParser,
) { ) {
@Throws(IncompatibleSheetStructure::class) @Throws(IncompatibleSheetStructure::class)
fun parse(sheet: ValueRange): List<MapDto> = parserScope { fun parse(sheet: ValueRange): List<LocationDataDbo> = parserScope(timeParser) {
val maps = mutableListOf<MapDto>() val maps = mutableListOf<LocationDataDbo>()
sheet.forEachDataLine(columns = COLUMNS) { sheet.forEachDataLine(columns = COLUMNS) {
val id = it.parse(column = ID)
val name = it.parse(column = NAME) val name = it.parse(column = NAME)
if (name != null) { if (id != null && name != null) {
val map = MapDto( val map = LocationDataDbo(
id = id,
name = name, name = name,
category = it.parse(column = CATEGORY), category = it.parse(column = CATEGORY),
uri = it.parseUri(column = URI),
description = it.parse(column = DESCRIPTION), 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) maps.add(map)
} }
@ -33,20 +35,16 @@ class MapParser @Inject constructor(
return@parserScope maps return@parserScope maps
} }
data class MapDto(
val name: String,
val category: String?,
val uri: Uri?,
val description: String?,
val illustrations: List<Uri>,
)
companion object { companion object {
private val ID = column("Id")
private val NAME = column("Nom") private val NAME = column("Nom")
private val CATEGORY = column("Catégorie") private val CATEGORY = column("Catégorie")
private val URI = column("Carte") private val MAP = column("Carte")
private val DESCRIPTION = column("Description") private val DESCRIPTION = column("Description")
private val ILLUSTRATIONS = column("Illustrations") 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)
} }
} }

View file

@ -1,7 +1,7 @@
package com.pixelized.rplexicon.data.parser.map package com.pixelized.rplexicon.data.parser.map
import androidx.compose.ui.geometry.Offset
import com.google.api.services.sheets.v4.model.ValueRange 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.column
import com.pixelized.rplexicon.data.parser.parserScope import com.pixelized.rplexicon.data.parser.parserScope
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
@ -10,22 +10,28 @@ import javax.inject.Inject
class WorldParser @Inject constructor() { class WorldParser @Inject constructor() {
@Throws(IncompatibleSheetStructure::class) @Throws(IncompatibleSheetStructure::class)
fun parse(sheet: ValueRange): List<WorldDto> = parserScope { fun parse(sheet: ValueRange): List<WorldDbo> = parserScope {
val worlds = mutableListOf<WorldDto>() val worlds = mutableListOf<WorldDbo>()
sheet.forEachDataLine(columns = COLUMNS) { line -> sheet.forEachDataLine(columns = COLUMNS) { line ->
val parentId = line.parse(column = PARENT_ID)
val parent = line.parse(column = PARENT) val parent = line.parse(column = PARENT)
val childId = line.parse(column = CHILD_ID)
val child = line.parse(column = CHILD) val child = line.parse(column = CHILD)
val x = line.parseFloat(column = X) val x = line.parseFloat(column = X)
val y = line.parseFloat(column = Y) val y = line.parseFloat(column = Y)
if (child != null) {
val world = WorldDto( // We check but don't use the parent value, because of the pre validation of the datasheet
parent = parent, if (
parentId != null && parent != null &&
childId != null && child != null
) {
val world = WorldDbo(
parentId = parentId,
childId = childId,
child = child, child = child,
position = when { x = x,
x != null && y != null -> Offset(x = x, y = y) y = y,
else -> Offset.Unspecified
},
) )
worlds.add(world) worlds.add(world)
} }
@ -34,17 +40,13 @@ class WorldParser @Inject constructor() {
return@parserScope worlds return@parserScope worlds
} }
data class WorldDto(
val parent: String?,
val child: String,
val position: Offset,
)
companion object { companion object {
private val PARENT_ID = column("Id Parent")
private val PARENT = column("Parent") private val PARENT = column("Parent")
private val CHILD_ID = column("Id Enfant")
private val CHILD = column("Enfant") private val CHILD = column("Enfant")
private val X = column("X") private val X = column("X")
private val Y = column("Y") 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)
} }
} }

View file

@ -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. * Query the [Lexicon] from the backend.
* @throws IncompatibleSheetStructure if the data structure change and mandatory data are missing. * @throws IncompatibleSheetStructure if the data structure change and mandatory data are missing.

View file

@ -1,31 +1,67 @@
package com.pixelized.rplexicon.data.repository.lexicon 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.model.Location
import com.pixelized.rplexicon.data.parser.map.LocationParser 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.GoogleSheetServiceRepository
import com.pixelized.rplexicon.data.repository.LexiconBinder import com.pixelized.rplexicon.data.repository.LexiconBinder
import com.pixelized.rplexicon.utilitary.Update import com.pixelized.rplexicon.utilitary.Update
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure 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.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class LocationRepository @Inject constructor( class LocationRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository, 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<List<Location>>(emptyList()) private val _data = MutableStateFlow<List<Location>>(emptyList())
val data: StateFlow<List<Location>> get() = _data val data: StateFlow<List<Location>> get() = _data
var lastSuccessFullUpdate: Update = Update.INITIAL var lastSuccessFullUpdate: Update = Update.INITIAL
private set private set
fun find(id: String?): Location? { init {
return id?.let { _data.value.firstOrNull { it.id == id } } 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<Location> = 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) @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.MAP).execute() },
async { sheets.get(LexiconBinder.ID, LexiconBinder.WORLD).execute() }, async { sheets.get(LexiconBinder.ID, LexiconBinder.WORLD).execute() },
) )
val data = parser.parse(mapSheet = map, worldSheet = world) database.runInTransaction {
_data.emit(data) 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() 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 { companion object {
const val WORLD_SHEET_URL = const val WORLD_SHEET_URL =
"https://docs.google.com/spreadsheets/d/${LexiconBinder.ID}/edit#gid=1943877267" "https://docs.google.com/spreadsheets/d/${LexiconBinder.ID}/edit#gid=1943877267"

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository
import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument
import com.pixelized.rplexicon.ui.navigation.screens.locationDetailArgument 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.line
import com.pixelized.rplexicon.utilitary.table import com.pixelized.rplexicon.utilitary.table
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
@ -28,7 +32,9 @@ class LocationDetailViewModel @Inject constructor(
private val clipboard = application.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager private val clipboard = application.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val sheetUri = LocationRepository.WORLD_SHEET_URL val sheetUri = LocationRepository.WORLD_SHEET_URL
val location: State<LocationDetailUio?>
private val _location = mutableStateOf<LocationDetailUio?>(null)
val location: State<LocationDetailUio?> get() = _location
private val _selectedMarquee = mutableStateOf<Int?>(null) private val _selectedMarquee = mutableStateOf<Int?>(null)
val selectedMarquee: State<Int?> get() = _selectedMarquee val selectedMarquee: State<Int?> get() = _selectedMarquee
@ -44,25 +50,35 @@ class LocationDetailViewModel @Inject constructor(
init { init {
val argument = savedStateHandle.locationDetailArgument val argument = savedStateHandle.locationDetailArgument
val source = repository.find(id = argument.id)
location = mutableStateOf( viewModelScope.launch {
source?.let { launch(Dispatchers.IO) {
LocationDetailUio( // update the last read time for that lexicon
name = it.name, repository.updateReadTime(id = argument.id)
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,
)
}
)
} }
) 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) { fun onSelectMarquee(marqueeUio: MarqueeUio) {

View file

@ -38,6 +38,7 @@ data class LocationItemUio(
val id: String, val id: String,
val title: String, val title: String,
val placeholder: Boolean = false, val placeholder: Boolean = false,
val isNew: Boolean = false,
) { ) {
companion object { companion object {
fun preview( fun preview(
@ -73,6 +74,7 @@ fun LocationItem(
true -> Modifier.placeholder { true } true -> Modifier.placeholder { true }
else -> Modifier.alignByBaseline() else -> Modifier.alignByBaseline()
}, },
color = if (item.isNew) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
style = typography.base.titleMedium, style = typography.base.titleMedium,
text = LOS_FULL, text = LOS_FULL,
) )

View file

@ -55,8 +55,9 @@ class LocationViewModel @Inject constructor(
}, },
valueTransform = { entry -> valueTransform = { entry ->
LocationItemUio( LocationItemUio(
id = entry.name, id = entry.id,
title = entry.name, title = entry.name,
isNew = entry.isNew,
) )
} }
) )

View file

@ -1,13 +1,10 @@
package com.pixelized.rplexicon.ui.screens.quest.detail package com.pixelized.rplexicon.ui.screens.quest.detail
import android.util.Log
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.data.repository.lexicon.QuestRepository
import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument
import com.pixelized.rplexicon.ui.navigation.screens.questDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.questDetailArgument
@ -21,8 +18,6 @@ import javax.inject.Inject
class QuestDetailViewModel @Inject constructor( class QuestDetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
questRepository: QuestRepository, questRepository: QuestRepository,
lexiconRepository: LexiconRepository,
locationRepository: LocationRepository,
) : ViewModel() { ) : ViewModel() {
private val _quest = mutableStateOf<QuestDetailUio?>(null) private val _quest = mutableStateOf<QuestDetailUio?>(null)
val quest: State<QuestDetailUio?> get() = _quest val quest: State<QuestDetailUio?> get() = _quest
@ -46,13 +41,12 @@ class QuestDetailViewModel @Inject constructor(
background = source.entries.mapNotNull { it.background }.randomOrNull(), background = source.entries.mapNotNull { it.background }.randomOrNull(),
title = source.title, title = source.title,
steps = source.entries.map { entry -> steps = source.entries.map { entry ->
val location = locationRepository.find(id = entry.area)
QuestDetailUio.QuestStep( QuestDetailUio.QuestStep(
subtitle = entry.subtitle, subtitle = entry.subtitle,
giverId = lexiconRepository.getByNameFlow(name = entry.questGiver), giverId = entry.questGiverId,
giver = entry.questGiver, giver = entry.questGiverName,
placeId = location?.id, placeId = entry.locationId,
place = location?.name ?: entry.area, place = entry.locationName,
globalReward = entry.groupReward, globalReward = entry.groupReward,
individualReward = entry.individualReward, individualReward = entry.individualReward,
description = entry.description, description = entry.description,