Add a quest log feature

This commit is contained in:
Thomas Andres Gomez 2023-08-01 16:08:02 +02:00
parent b3d3e3e314
commit d55e782510
58 changed files with 1740 additions and 452 deletions

View file

@ -9,6 +9,10 @@ import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.sheets.v4.Sheets
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -59,7 +63,7 @@ class LexiconRepository @Inject constructor(
val lexicon: List<Lexicon> = sheet?.mapIndexedNotNull { index, row ->
when {
index == 0 -> {
sheetStructure = checkSheetStructure(firstRow = row)
sheetStructure = row.checkSheetStructure(model = Sheet.COLUMNS)
null
}
@ -79,34 +83,6 @@ class LexiconRepository @Inject constructor(
_data.tryEmit(lexicon)
}
@Throws(IncompatibleSheetStructure::class)
private fun checkSheetStructure(firstRow: Any?): HashMap<String, Int> {
// check if the row is a list
if (firstRow !is ArrayList<*>) {
throw IncompatibleSheetStructure("First row is not a List: $firstRow")
}
// parse the first line to find element that we recognize.
val sheetStructure = hashMapOf<String, Int>()
firstRow.forEachIndexed { index, cell ->
if (cell is String && Sheet.COLUMNS.contains(cell)) {
sheetStructure[cell] = index
}
}
// check if we found everything we need.
when {
sheetStructure.size < Sheet.COLUMNS.size -> throw IncompatibleSheetStructure(
message = "Sheet header row does not have enough column: ${firstRow.size}.\nstructure: $firstRow\nheader: $sheetStructure"
)
sheetStructure.size > Sheet.COLUMNS.size -> throw IncompatibleSheetStructure(
message = "Sheet header row does have too mush columns: ${firstRow.size}.\nstructure: $firstRow\nheader: $sheetStructure"
)
}
return sheetStructure
}
private fun parseCharacterRow(
sheetStructure: Map<String, Int>?,
id: Int,
@ -159,13 +135,6 @@ class LexiconRepository @Inject constructor(
}
}
private fun MutableCollection<Any>?.sheet(): List<*>? {
return this?.firstOrNull {
val sheet = it as? ArrayList<*>
sheet != null
} as List<*>?
}
private fun String?.toUriOrNull(): Uri? = try {
this?.takeIf { it.isNotBlank() }?.toUri()
} catch (_: Exception) {
@ -181,10 +150,6 @@ class LexiconRepository @Inject constructor(
private val Map<String, Int>?.history: Int get() = this?.getValue(Sheet.HISTORY) ?: 6
private val Map<String, Int>?.tags: Int get() = this?.getValue(Sheet.TAGS) ?: 7
class ServiceNotReady : Exception()
class IncompatibleSheetStructure(message: String?) : Exception(message)
companion object {
const val TAG = "LexiconRepository"
}

View file

@ -0,0 +1,156 @@
package com.pixelized.rplexicon.repository
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import com.google.api.client.extensions.android.http.AndroidHttp
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.sheets.v4.Sheets
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Quest
import com.pixelized.rplexicon.model.QuestEntry
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class QuestRepository @Inject constructor(
private val authenticationRepository: AuthenticationRepository,
) {
private val sheetService: Sheets? by derivedStateOf {
when (authenticationRepository.isAuthenticated.value) {
true -> Sheets
.Builder(
AndroidHttp.newCompatibleTransport(),
GsonFactory(),
authenticationRepository.credential,
)
.build()
else -> null
}
}
private val _data = MutableStateFlow<List<Quest>>(emptyList())
val data: StateFlow<List<Quest>> get() = _data
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchQuests() {
val service = sheetService
if (service == null) {
throw ServiceNotReady()
} else {
withContext(Dispatchers.IO) {
val request = service.spreadsheets().values().get(Sheet.ID, Sheet.QUEST_JOURNAL)
val data = request.execute()
updateData(data = data)
}
}
}
@Throws(IncompatibleSheetStructure::class)
private fun updateData(data: ValueRange?) {
val sheet = data?.values?.sheet()
var sheetStructure: Map<String, Int>? = null
val questEntries: List<QuestEntry> = sheet?.mapIndexedNotNull { index, row ->
when {
index == 0 -> {
sheetStructure = row.checkSheetStructure(model = Sheet.COLUMNS)
null
}
row is List<*> -> parseQuestRow(
sheetStructure = sheetStructure,
sheetIndex = index,
row = row,
)
else -> null
}
} ?: emptyList()
val questMap = questEntries.groupBy { it.title }
val quests = questMap.keys.mapIndexed { index, item ->
Quest(
id = index,
title = item,
entries = questMap[item] ?: emptyList(),
)
}
_data.tryEmit(quests)
}
private fun parseQuestRow(
sheetStructure: Map<String, Int>?,
sheetIndex: Int,
row: List<*>?,
): QuestEntry? {
val title = row?.getOrNull(sheetStructure.title) as? String
val subtitle = row?.getOrNull(sheetStructure.subtitle) as? String?
val complete = row?.getOrNull(sheetStructure.complete) as? Boolean? ?: false
val questGiver = row?.getOrNull(sheetStructure.questGiver) as? String?
val area = row?.getOrNull(sheetStructure.area) as? String?
val groupReward = row?.getOrNull(sheetStructure.groupReward) as? String?
val individualReward = row?.getOrNull(sheetStructure.individualReward) as? String?
val description = row?.getOrNull(sheetStructure.description) as? String
return if (title?.isNotEmpty() == true && description?.isNotEmpty() == true) {
QuestEntry(
sheetIndex = sheetIndex,
title = title,
subtitle = subtitle?.takeIf { it.isNotBlank() },
complete = complete,
questGiver = questGiver?.takeIf { it.isNotBlank() },
area = area?.takeIf { it.isNotBlank() },
groupReward = groupReward?.takeIf { it.isNotBlank() },
individualReward = individualReward?.takeIf { it.isNotBlank() },
description = description,
)
} else {
null
}
}
private val Map<String, Int>?.title: Int get() = this?.getValue(Sheet.TITLE) ?: 0
private val Map<String, Int>?.subtitle: Int get() = this?.getValue(Sheet.SUBTITLE) ?: 1
private val Map<String, Int>?.complete: Int get() = this?.getValue(Sheet.COMPLETE) ?: 2
private val Map<String, Int>?.questGiver: Int get() = this?.getValue(Sheet.QUEST_GIVER) ?: 3
private val Map<String, Int>?.area: Int get() = this?.getValue(Sheet.AREA) ?: 4
private val Map<String, Int>?.groupReward: Int get() = this?.getValue(Sheet.G_REWARD) ?: 5
private val Map<String, Int>?.individualReward: Int get() = this?.getValue(Sheet.I_REWARD) ?: 6
private val Map<String, Int>?.description: Int get() = this?.getValue(Sheet.DESCRIPTION) ?: 7
private object Sheet {
const val ID = "1sDAay8DjbRYKM39MvEXWs-RuvyxjOFpOfRZLAEWjIUY"
const val QUEST_JOURNAL = "Journal de quêtes"
val COLUMNS = listOf(
"Titre",
"Sous Titre",
"Compléter",
"Commanditaire",
"Lieu",
"Récompense de groupe",
"Récompense individuelle",
"Description",
)
val TITLE = COLUMNS[0]
val SUBTITLE = COLUMNS[1]
val COMPLETE = COLUMNS[2]
val QUEST_GIVER = COLUMNS[3]
val AREA = COLUMNS[4]
val G_REWARD = COLUMNS[5]
val I_REWARD = COLUMNS[6]
val DESCRIPTION = COLUMNS[7]
}
}