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

View file

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

View file

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

View file

@ -16,9 +16,6 @@ interface LexiconDao {
@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(onConflict = OnConflictStrategy.IGNORE)
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 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,

View file

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

View file

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

View file

@ -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<Location> {
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<String, List<WorldDbo>>): 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<LocationDbo>, world: List<WorldDbo>): List<Location> {
val worldHash = world.groupBy { it.parentId }
return locations.map { convert(location = it, world = worldHash) }
}
}

View file

@ -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<MapDto> = parserScope {
val maps = mutableListOf<MapDto>()
fun parse(sheet: ValueRange): List<LocationDataDbo> = parserScope(timeParser) {
val maps = mutableListOf<LocationDataDbo>()
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<Uri>,
)
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)
}
}

View file

@ -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<WorldDto> = parserScope {
val worlds = mutableListOf<WorldDto>()
fun parse(sheet: ValueRange): List<WorldDbo> = parserScope {
val worlds = mutableListOf<WorldDbo>()
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)
}
}

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.
* @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
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<List<Location>>(emptyList())
val data: StateFlow<List<Location>> 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<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)
@ -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"

View file

@ -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<LocationDetailUio?>
private val _location = mutableStateOf<LocationDetailUio?>(null)
val location: State<LocationDetailUio?> get() = _location
private val _selectedMarquee = mutableStateOf<Int?>(null)
val selectedMarquee: State<Int?> 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) {

View file

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

View file

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

View file

@ -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<QuestDetailUio?>(null)
val quest: State<QuestDetailUio?> 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,