Refactor repositories by adding parsers.

Add character sheet poc.
This commit is contained in:
Thomas Andres Gomez 2023-08-03 16:11:20 +02:00
parent bb760392f0
commit 87a1471efe
17 changed files with 761 additions and 286 deletions

View file

@ -2,6 +2,7 @@ package com.pixelized.rplexicon
import android.app.Activity
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.WindowInsets
@ -19,6 +20,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.pixelized.rplexicon.facotry.RollParser
import com.pixelized.rplexicon.ui.navigation.ScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import dagger.hilt.android.AndroidEntryPoint

View file

@ -0,0 +1,20 @@
package com.pixelized.rplexicon.facotry
import com.pixelized.rplexicon.model.Lexicon
import javax.inject.Inject
class GenderParser @Inject constructor() {
fun parse(gender: String?): Lexicon.Gender {
return when (gender?.takeIf { it.isNotBlank() }) {
Gender.MALE -> Lexicon.Gender.MALE
Gender.FEMALE -> Lexicon.Gender.FEMALE
else -> Lexicon.Gender.UNDETERMINED
}
}
private object Gender {
const val MALE = "Male"
const val FEMALE = "Femelle"
}
}

View file

@ -0,0 +1,83 @@
package com.pixelized.rplexicon.facotry
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.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class LexiconParser @Inject constructor(
private val portraitParser: PortraitParser,
private val genderParser: GenderParser,
private val raceParser: RaceParser,
) {
@Throws(IncompatibleSheetStructure::class)
fun parse(data: ValueRange): List<Lexicon> {
val sheet = data.values.sheet()
lateinit var sheetStructure: Map<String, Int>
var id = 0
return sheet?.mapIndexedNotNull { index, row ->
when {
index == 0 -> {
sheetStructure = row.checkSheetStructure(model = COLUMNS)
null
}
row is List<*> -> {
val name = row.getOrNull(sheetStructure.name) as? String
val diminutive = row.getOrNull(sheetStructure.diminutive) as? String?
val gender = row.getOrNull(sheetStructure.gender) as? String?
val race = row.getOrNull(sheetStructure.race) as? String?
val portrait = row.getOrNull(sheetStructure.portrait) as? String?
val description = row.getOrNull(sheetStructure.description) as? String?
val history = row.getOrNull(sheetStructure.history) as? String?
val tags = row.getOrNull(sheetStructure.tags) as? String?
if (name != null) {
Lexicon(
id = id++,
sheetIndex = index,
name = name,
diminutive = diminutive?.takeIf { it.isNotBlank() },
gender = genderParser.parse(gender),
race = raceParser.parser(race),
portrait = portraitParser.parse(portrait),
description = description?.takeIf { it.isNotBlank() },
history = history?.takeIf { it.isNotBlank() },
tags = tags?.takeIf { it.isNotBlank() },
)
} else {
null
}
}
else -> null
}
} ?: emptyList()
}
private val Map<String, Int>.name: Int get() = getValue(COLUMNS[0])
private val Map<String, Int>.diminutive: Int get() = getValue(COLUMNS[1])
private val Map<String, Int>.gender: Int get() = getValue(COLUMNS[2])
private val Map<String, Int>.race: Int get() = getValue(COLUMNS[3])
private val Map<String, Int>.portrait: Int get() = getValue(COLUMNS[4])
private val Map<String, Int>.description: Int get() = getValue(COLUMNS[5])
private val Map<String, Int>.history: Int get() = getValue(COLUMNS[6])
private val Map<String, Int>.tags: Int get() = getValue(COLUMNS[7])
companion object {
private val COLUMNS = listOf(
"Nom",
"Diminutif",
"Sexe",
"Race",
"Portrait",
"Description",
"Histoire",
"Mots clés",
)
}
}

View file

@ -0,0 +1,54 @@
package com.pixelized.rplexicon.facotry
import android.net.Uri
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Location
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class LocationParser @Inject constructor() {
@Throws(IncompatibleSheetStructure::class)
fun parse(data: ValueRange): List<Location> {
var id = 0
var sheetStructure: Map<String, Int>? = null
return data.values.sheet()?.mapIndexedNotNull { index, item ->
when {
index == 0 -> {
sheetStructure = item.checkSheetStructure(COLUMNS)
null
}
item is List<*> -> {
val name = item.getOrNull(sheetStructure.name) as? String
val uri = item.getOrNull(sheetStructure.uri)?.toString()?.let { Uri.parse(it) }
if (name != null && uri != null) {
Location(
id = id++,
sheetIndex = index,
name = name,
uri = uri,
)
} else {
null
}
}
else -> null
}
} ?: emptyList()
}
private val Map<String, Int>?.name: Int get() = this?.getValue(COLUMNS[0]) ?: 0
private val Map<String, Int>?.uri: Int get() = this?.getValue(COLUMNS[1]) ?: 1
companion object {
private val COLUMNS = listOf(
"nom", "carte"
)
}
}

View file

@ -0,0 +1,58 @@
package com.pixelized.rplexicon.facotry
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Location
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class MarqueeParser @Inject constructor() {
fun parse(data: ValueRange): List<Location.Marquee> {
val sheet = data.values.sheet()
lateinit var structure: Map<String, Int>
return sheet?.mapIndexedNotNull { index, item ->
when {
index == 0 -> {
structure = item.checkSheetStructure(model = COLUMNS)
null
}
item is List<*> -> {
val map = item.getOrNull(structure.map) as? String
val name = item.getOrNull(structure.name) as? String
val x = (item.getOrNull(structure.x) as? String)?.toFloatOrNull()
val y = (item.getOrNull(structure.y) as? String)?.toFloatOrNull()
val description = item.getOrNull(structure.description) as? String?
if (map != null && name != null && x != null && y != null) {
Location.Marquee(
map = map,
name = name,
x = x,
y = y,
description = description,
)
} else {
null
}
}
else -> null
}
} ?: emptyList()
}
private val Map<String, Int>.map: Int get() = getValue(COLUMNS[0])
private val Map<String, Int>.name: Int get() = getValue(COLUMNS[1])
private val Map<String, Int>.x: Int get() = getValue(COLUMNS[2])
private val Map<String, Int>.y: Int get() = getValue(COLUMNS[3])
private val Map<String, Int>.description: Int get() = getValue(COLUMNS[4])
companion object {
private val COLUMNS = listOf(
"carte", "nom", "x", "y", "description"
)
}
}

View file

@ -0,0 +1,16 @@
package com.pixelized.rplexicon.facotry
import android.net.Uri
import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull
import javax.inject.Inject
class PortraitParser @Inject constructor() {
fun parse(value: String?): List<Uri> {
return value?.split(SEPARATOR)?.mapNotNull { it.toUriOrNull() } ?: emptyList()
}
companion object {
private const val SEPARATOR = "\n"
}
}

View file

@ -0,0 +1,81 @@
package com.pixelized.rplexicon.facotry
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.QuestEntry
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull
import javax.inject.Inject
class QuestParser @Inject constructor() {
fun parse(value: ValueRange): List<QuestEntry> {
val sheet = value.values.sheet()
lateinit var structure: Map<String, Int>
return sheet?.mapIndexedNotNull { index, item ->
when {
index == 0 -> {
structure = item.checkSheetStructure(model = COLUMNS)
null
}
item is List<*> -> {
val title = item.getOrNull(structure.title) as? String
val subtitle = item.getOrNull(structure.subtitle) as? String?
val complete = item.getOrNull(structure.complete) as? String?
val questGiver = item.getOrNull(structure.questGiver) as? String?
val area = item.getOrNull(structure.area) as? String?
val groupReward = item.getOrNull(structure.groupReward) as? String?
val individualReward = item.getOrNull(structure.individualReward) as? String?
val description = item.getOrNull(structure.description) as? String
val background = item.getOrNull(structure.background) as? String?
if (title?.isNotEmpty() == true && description?.isNotEmpty() == true) {
QuestEntry(
sheetIndex = index,
title = title,
subtitle = subtitle?.takeIf { it.isNotBlank() },
complete = complete.equals("TRUE", ignoreCase = true),
questGiver = questGiver?.takeIf { it.isNotBlank() },
area = area?.takeIf { it.isNotBlank() },
groupReward = groupReward?.takeIf { it.isNotBlank() },
individualReward = individualReward?.takeIf { it.isNotBlank() },
description = description,
background = background?.toUriOrNull(),
)
} else {
null
}
}
else -> null
}
} ?: emptyList()
}
private val Map<String, Int>.title: Int get() = getValue(COLUMNS[0])
private val Map<String, Int>.subtitle: Int get() = getValue(COLUMNS[1])
private val Map<String, Int>.complete: Int get() = getValue(COLUMNS[2])
private val Map<String, Int>.questGiver: Int get() = getValue(COLUMNS[3])
private val Map<String, Int>.area: Int get() = getValue(COLUMNS[4])
private val Map<String, Int>.groupReward: Int get() = getValue(COLUMNS[5])
private val Map<String, Int>.individualReward: Int get() = getValue(COLUMNS[6])
private val Map<String, Int>.description: Int get() = getValue(COLUMNS[7])
private val Map<String, Int>.background: Int get() = getValue(COLUMNS[8])
companion object {
private val COLUMNS = listOf(
"Titre",
"Sous Titre",
"Compléter",
"Commanditaire",
"Lieu",
"Récompense de groupe",
"Récompense individuelle",
"Description",
"fond"
)
}
}

View file

@ -0,0 +1,42 @@
package com.pixelized.rplexicon.facotry
import com.pixelized.rplexicon.model.Lexicon
import javax.inject.Inject
class RaceParser @Inject constructor() {
fun parser(race: String?): Lexicon.Race {
return when (race?.takeIf { it.isNotBlank() }) {
Race.ELF -> Lexicon.Race.ELF
Race.HALFLING -> Lexicon.Race.HALFLING
Race.HUMAN -> Lexicon.Race.HUMAN
Race.DWARF -> Lexicon.Race.DWARF
Race.HALF_ELF -> Lexicon.Race.HALF_ELF
Race.HALF_ORC -> Lexicon.Race.HALF_ORC
Race.DRAGONBORN -> Lexicon.Race.DRAGONBORN
Race.GNOME -> Lexicon.Race.GNOME
Race.TIEFLING -> Lexicon.Race.TIEFLING
Race.AARAKOCRA -> Lexicon.Race.AARAKOCRA
Race.GENASI -> Lexicon.Race.GENASI
Race.DEEP_GNOME -> Lexicon.Race.DEEP_GNOME
Race.GOLIATH -> Lexicon.Race.GOLIATH
else -> Lexicon.Race.UNDETERMINED
}
}
private object Race {
const val ELF = "Elfe"
const val HALFLING = "Halfelin"
const val HUMAN = "Humain"
const val DWARF = "Nain"
const val HALF_ELF = "Demi-Elfe"
const val HALF_ORC = "Demi-Orc"
const val DRAGONBORN = "Drakéide"
const val GNOME = "Gnome"
const val TIEFLING = "Tieffelin"
const val AARAKOCRA = "Aarakocra"
const val GENASI = "Génasi"
const val DEEP_GNOME = "Gnome des Profondeurs"
const val GOLIATH = "Goliath"
}
}

View file

@ -0,0 +1,49 @@
package com.pixelized.rplexicon.facotry
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.Roll
import javax.inject.Inject
class RollParser @Inject constructor() {
private val diceRegex = Regex("(\\d+)d(\\d+)")
private val bonusRegex = Regex("(?:[a-zA-Z]|\\?)[a-zA-Z]+")
fun parseRoll(characterSheet: CharacterSheet, value: String?): Roll {
val roll = value?.split(";")
val label = roll?.getOrNull(0)
val (dices, bonus) = roll?.getOrNull(1)?.let { item ->
val dices = diceRegex.findAll(item).toList().map { it.parseDice() }
val bonus = bonusRegex.findAll(item).map { bonus ->
Roll.Bonus(
label = bonus.value,
bonus = bonus.value.parseBonus(characterSheet = characterSheet),
)
}
dices.toList() to bonus.toList()
} ?: (null to null)
return Roll(
label = label.toString(),
dices = dices ?: emptyList(),
bonus = bonus ?: emptyList(),
)
}
private fun String?.parseBonus(
characterSheet: CharacterSheet,
): Int = when (this?.lowercase()) {
"bonus" -> characterSheet.proficiency
"force" -> characterSheet.strengthBonus
else -> 0
}
private fun MatchResult.parseDice(): Roll.Dice {
val (count, faces) = destructured
return Roll.Dice(
label = value,
count = count.toIntOrNull() ?: 0,
faces = faces.toIntOrNull() ?: 0,
)
}
}

View file

@ -0,0 +1,84 @@
package com.pixelized.rplexicon.model
import androidx.compose.runtime.Stable
@Stable
data class CharacterSheet(
val hitPoint: Int, // Point de vie
val armorClass: Int, // Classe d'armure
val proficiency: Int, // Bonus de maîtrise
val strength: Int, // Force
val dexterity: Int, // Dextérité
val constitution: Int, // Constitution
val intelligence: Int, // Intelligence
val wisdom: Int, // Sagesse
val charisma: Int, // Charisme
val strengthSavingThrows: Int, // Jet de sauvegarde: Force
val dexteritySavingThrows: Int, // Jet de sauvegarde: Dextérité
val constitutionSavingThrows: Int, // Jet de sauvegarde: Constitution
val intelligenceSavingThrows: Int, // Jet de sauvegarde: Intelligence
val wisdomSavingThrows: Int, // Jet de sauvegarde: Sagesse
val charismaSavingThrows: Int, // Jet de sauvegarde: Charisme
val acrobatics: Int, // DEX, Acrobaties
val animalHandling: Int, // WIS, Arcanes
val arcana: Int, // INT, Athlétisme
val athletics: Int, // STR, Discrétion
val deception: Int, // CHA, Dressage
val history: Int, // INT, Escamotage
val insight: Int, // WIS, Histoire
val intimidation: Int, // CHA, Intimidation
val investigation: Int, // INT, Intuition
val medicine: Int, // WIS, Investigation
val nature: Int, // INT, Médecine
val perception: Int, // WIS, Nature
val performance: Int, // CHA, Perception
val persuasion: Int, // CHA, Persuasion
val religion: Int, // INT, Religion
val sleightOfHand: Int, // DEX, Représentation
val stealth: Int, // DEX, Survie
val survival: Int, // WIS, Tromperie
) {
val proficiencyBonus: Int = kotlin.math.floor(proficiency / 2 - 5f).toInt()
val strengthBonus: Int = kotlin.math.floor(strength / 2 - 5f).toInt()
val dexterityBonus: Int = kotlin.math.floor(dexterity / 2 - 5f).toInt()
val constitutionBonus: Int = kotlin.math.floor(constitution / 2 - 5f).toInt()
val intelligenceBonus: Int = kotlin.math.floor(intelligence / 2 - 5f).toInt()
}
@Stable
data class Roll(
val label: String,
val dices: List<Dice>,
val bonus: List<Bonus>,
) {
@Stable
data class Dice(
val label: String,
val count: Int,
val faces: Int,
)
@Stable
data class Bonus(
val label: String,
val bonus: Int,
)
}

View file

@ -0,0 +1,22 @@
package com.pixelized.rplexicon.model
import android.net.Uri
import androidx.compose.runtime.Stable
@Stable
data class Location(
val id: Int,
val sheetIndex: Int,
val name: String,
val uri: Uri,
val marquees: List<Marquee>,
) {
@Stable
data class Marquee(
val map: String,
val name: String,
val x: Float,
val y: Float,
val description: String?,
)
}

View file

@ -0,0 +1,120 @@
package com.pixelized.rplexicon.repository
import android.util.Log
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.RollParser
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CharacterSheetRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository,
private val rollParser: RollParser,
) {
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchCharacterSheet() {
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.ID, Sheet.CHARACTER_SHEET)
val data = request.execute()
updateData(data = data)
}
}
@Throws(IncompatibleSheetStructure::class)
private fun updateData(data: ValueRange?) {
val sheet = data?.values?.sheet()
var id = 0
val bru = sheet?.map { (it as? List<*>)?.get(1) }
val characterSheet = CharacterSheet(
hitPoint = (bru?.get(Sheet.HIT_POINT) as? String)?.toIntOrNull() ?: 0,
armorClass = (bru?.get(Sheet.ARMOR_CLASS) as? String)?.toIntOrNull() ?: 0,
proficiency = (bru?.get(Sheet.PROFICIENCY) as? String)?.toIntOrNull() ?: 0,
strength = (bru?.get(Sheet.STRENGTH) as? String)?.toIntOrNull() ?: 0,
dexterity = (bru?.get(Sheet.DEXTERITY) as? String)?.toIntOrNull() ?: 0,
constitution = (bru?.get(Sheet.CONSTITUTION) as? String)?.toIntOrNull() ?: 0,
intelligence = (bru?.get(Sheet.INTELLIGENCE) as? String)?.toIntOrNull() ?: 0,
wisdom = (bru?.get(Sheet.WISDOM) as? String)?.toIntOrNull() ?: 0,
charisma = (bru?.get(Sheet.CHARISMA) as? String)?.toIntOrNull() ?: 0,
strengthSavingThrows = (bru?.get(Sheet.STRENGTH_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0,
dexteritySavingThrows = (bru?.get(Sheet.DEXTERITY_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0,
constitutionSavingThrows = (bru?.get(Sheet.CONSTITUTION_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0,
intelligenceSavingThrows = (bru?.get(Sheet.INTELLIGENCE_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0,
wisdomSavingThrows = (bru?.get(Sheet.WISDOM_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0,
charismaSavingThrows = (bru?.get(Sheet.CHARISMA_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0,
acrobatics = (bru?.get(Sheet.ACROBATICS) as? String)?.toIntOrNull() ?: 0,
animalHandling = (bru?.get(Sheet.ANIMAL_HANDLING) as? String)?.toIntOrNull() ?: 0,
arcana = (bru?.get(Sheet.ARCANA) as? String)?.toIntOrNull() ?: 0,
athletics = (bru?.get(Sheet.ATHLETICS) as? String)?.toIntOrNull() ?: 0,
deception = (bru?.get(Sheet.DECEPTION) as? String)?.toIntOrNull() ?: 0,
history = (bru?.get(Sheet.HISTORY) as? String)?.toIntOrNull() ?: 0,
insight = (bru?.get(Sheet.INSIGHT) as? String)?.toIntOrNull() ?: 0,
intimidation = (bru?.get(Sheet.INTIMIDATION) as? String)?.toIntOrNull() ?: 0,
investigation = (bru?.get(Sheet.INVESTIGATION) as? String)?.toIntOrNull() ?: 0,
medicine = (bru?.get(Sheet.MEDICINE) as? String)?.toIntOrNull() ?: 0,
nature = (bru?.get(Sheet.NATURE) as? String)?.toIntOrNull() ?: 0,
perception = (bru?.get(Sheet.PERCEPTION) as? String)?.toIntOrNull() ?: 0,
performance = (bru?.get(Sheet.PERFORMANCE) as? String)?.toIntOrNull() ?: 0,
persuasion = (bru?.get(Sheet.PERSUASION) as? String)?.toIntOrNull() ?: 0,
religion = (bru?.get(Sheet.RELIGION) as? String)?.toIntOrNull() ?: 0,
sleightOfHand = (bru?.get(Sheet.SLEIGHT_OF_HAND) as? String)?.toIntOrNull() ?: 0,
stealth = (bru?.get(Sheet.STEALTH) as? String)?.toIntOrNull() ?: 0,
survival = (bru?.get(Sheet.SURVIVAL) as? String)?.toIntOrNull() ?: 0,
)
val rolls = bru?.subList(fromIndex = 34, bru.size)?.mapNotNull {
rollParser.parseRoll(characterSheet = characterSheet, value = it?.toString())
}
Log.e(TAG, characterSheet.toString())
Log.e(TAG, rolls.toString())
}
companion object {
const val TAG = "CharacterSheetRepository"
}
private object Sheet {
const val ID = "1fHfzeb8y5u9lEQB1iI-jBEhqu7YSip5sAajXcXK7VJ8"
const val CHARACTER_SHEET = "Feuille de personnage"
const val HIT_POINT = 1
const val ARMOR_CLASS = 2
const val PROFICIENCY = 3
const val STRENGTH = 4
const val DEXTERITY = 5
const val CONSTITUTION = 6
const val INTELLIGENCE = 7
const val WISDOM = 8
const val CHARISMA = 9
const val STRENGTH_SAVING_THROWS = 10
const val DEXTERITY_SAVING_THROWS = 11
const val CONSTITUTION_SAVING_THROWS = 12
const val INTELLIGENCE_SAVING_THROWS = 13
const val WISDOM_SAVING_THROWS = 14
const val CHARISMA_SAVING_THROWS = 15
const val ACROBATICS = 16
const val ANIMAL_HANDLING = 17
const val ARCANA = 18
const val ATHLETICS = 19
const val DECEPTION = 20
const val HISTORY = 21
const val INSIGHT = 22
const val INTIMIDATION = 23
const val INVESTIGATION = 24
const val MEDICINE = 25
const val NATURE = 26
const val PERCEPTION = 27
const val PERFORMANCE = 28
const val PERSUASION = 29
const val RELIGION = 30
const val SLEIGHT_OF_HAND = 31
const val STEALTH = 32
const val SURVIVAL = 33
}
}

View file

@ -0,0 +1,44 @@
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.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GoogleSheetServiceRepository @Inject constructor(
authenticationRepository: AuthenticationRepository,
) {
private val service: Sheets? by derivedStateOf {
when (authenticationRepository.isAuthenticated.value) {
true -> Sheets
.Builder(
AndroidHttp.newCompatibleTransport(),
GsonFactory(),
authenticationRepository.credential,
)
.build()
else -> null
}
}
@Throws(ServiceNotReady::class)
suspend fun fetch(
lambda: suspend CoroutineScope.(service: Sheets.Spreadsheets.Values) -> Unit,
) {
when (val service = service) {
null -> throw ServiceNotReady()
else -> withContext(Dispatchers.IO) {
lambda(service.spreadsheets().values())
}
}
}
}

View file

@ -1,42 +1,20 @@
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.facotry.LexiconParser
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 com.pixelized.rplexicon.utilitary.extentions.toUriOrNull
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 LexiconRepository @Inject constructor(
private val authenticationRepository: AuthenticationRepository
private val googleRepository: GoogleSheetServiceRepository,
private val lexiconParser: LexiconParser,
) {
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<Lexicon>>(emptyList())
val data: StateFlow<List<Lexicon>> get() = _data
@ -46,156 +24,25 @@ class LexiconRepository @Inject constructor(
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchLexicon() {
val service = sheetService
if (service == null) {
throw ServiceNotReady()
} else {
withContext(Dispatchers.IO) {
val request = service.spreadsheets().values().get(Sheet.ID, Sheet.LEXIQUE)
val data = request.execute()
updateData(data = data)
}
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.ID, Sheet.LEXICON)
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
var id = 0
val lexicon: List<Lexicon> = sheet?.mapIndexedNotNull { index, row ->
when {
index == 0 -> {
sheetStructure = row.checkSheetStructure(model = Sheet.COLUMNS)
null
}
row is List<*> -> parseCharacterRow(
sheetStructure = sheetStructure,
id = id,
sheetIndex = index,
row = row,
)?.also {
// update next id if parsing is successful.
id = it.id + 1
}
else -> null
}
} ?: emptyList()
private fun updateData(data: ValueRange) {
val lexicon = lexiconParser.parse(data)
_data.tryEmit(lexicon)
}
private fun parseCharacterRow(
sheetStructure: Map<String, Int>?,
id: Int,
sheetIndex: Int,
row: List<*>?,
): Lexicon? {
val name = row?.getOrNull(sheetStructure.name) as? String
val diminutive = row?.getOrNull(sheetStructure.diminutive) as? String?
val gender = row?.getOrNull(sheetStructure.gender) as? String?
val race = row?.getOrNull(sheetStructure.race) as? String?
val portrait = row?.getOrNull(sheetStructure.portrait) as? String?
val description = row?.getOrNull(sheetStructure.description) as? String?
val history = row?.getOrNull(sheetStructure.history) as? String?
val tags = row?.getOrNull(sheetStructure.tags) as? String?
return if (name != null) {
Lexicon(
id = id,
sheetIndex = sheetIndex,
name = name,
diminutive = diminutive?.takeIf { it.isNotBlank() },
gender = when (gender?.takeIf { it.isNotBlank() }) {
Gender.MALE -> Lexicon.Gender.MALE
Gender.FEMALE -> Lexicon.Gender.FEMALE
else -> Lexicon.Gender.UNDETERMINED
},
race = when (race?.takeIf { it.isNotBlank() }) {
Race.ELF -> Lexicon.Race.ELF
Race.HALFLING -> Lexicon.Race.HALFLING
Race.HUMAN -> Lexicon.Race.HUMAN
Race.DWARF -> Lexicon.Race.DWARF
Race.HALF_ELF -> Lexicon.Race.HALF_ELF
Race.HALF_ORC -> Lexicon.Race.HALF_ORC
Race.DRAGONBORN -> Lexicon.Race.DRAGONBORN
Race.GNOME -> Lexicon.Race.GNOME
Race.TIEFLING -> Lexicon.Race.TIEFLING
Race.AARAKOCRA -> Lexicon.Race.AARAKOCRA
Race.GENASI -> Lexicon.Race.GENASI
Race.DEEP_GNOME -> Lexicon.Race.DEEP_GNOME
Race.GOLIATH -> Lexicon.Race.GOLIATH
else -> Lexicon.Race.UNDETERMINED
},
portrait = portrait?.split("\n")?.mapNotNull { it.toUriOrNull() } ?: emptyList(),
description = description?.takeIf { it.isNotBlank() },
history = history?.takeIf { it.isNotBlank() },
tags = tags?.takeIf { it.isNotBlank() },
)
} else {
null
}
}
private val Map<String, Int>?.name: Int get() = this?.getValue(Sheet.NAME) ?: 0
private val Map<String, Int>?.diminutive: Int get() = this?.getValue(Sheet.DIMINUTIVE) ?: 1
private val Map<String, Int>?.gender: Int get() = this?.getValue(Sheet.GENDER) ?: 2
private val Map<String, Int>?.race: Int get() = this?.getValue(Sheet.RACE) ?: 3
private val Map<String, Int>?.portrait: Int get() = this?.getValue(Sheet.PORTRAIT) ?: 4
private val Map<String, Int>?.description: Int get() = this?.getValue(Sheet.DESCRIPTION) ?: 5
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
companion object {
const val TAG = "LexiconRepository"
}
private object Sheet {
const val ID = "1oL9Nu5y37BPEbKxHre4TN9o8nrgy2JQoON4RRkdAHMs"
const val LEXIQUE = "Lexique"
const val META = "MetaData"
val COLUMNS = listOf(
"Nom",
"Diminutif",
"Sexe",
"Race",
"Portrait",
"Description",
"Histoire",
"Mots clés",
)
val NAME = COLUMNS[0]
val DIMINUTIVE = COLUMNS[1]
val GENDER = COLUMNS[2]
val RACE = COLUMNS[3]
val PORTRAIT = COLUMNS[4]
val DESCRIPTION = COLUMNS[5]
val HISTORY = COLUMNS[6]
val TAGS = COLUMNS[7]
}
private object Gender {
const val MALE = "Male"
const val FEMALE = "Femelle"
}
private object Race {
const val ELF = "Elfe"
const val HALFLING = "Halfelin"
const val HUMAN = "Humain"
const val DWARF = "Nain"
const val HALF_ELF = "Demi-Elfe"
const val HALF_ORC = "Demi-Orc"
const val DRAGONBORN = "Drakéide"
const val GNOME = "Gnome"
const val TIEFLING = "Tieffelin"
const val AARAKOCRA = "Aarakocra"
const val GENASI = "Génasi"
const val DEEP_GNOME = "Gnome des Profondeurs"
const val GOLIATH = "Goliath"
const val LEXICON = "Lexique"
}
}

View file

@ -0,0 +1,66 @@
package com.pixelized.rplexicon.repository
import com.google.api.services.sheets.v4.Sheets
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.LocationParser
import com.pixelized.rplexicon.facotry.MarqueeParser
import com.pixelized.rplexicon.model.Location
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository,
private val locationParser: LocationParser,
private val marqueeParser: MarqueeParser,
) {
private val _data = MutableStateFlow<List<Location>>(emptyList())
val data: StateFlow<List<Location>> get() = _data
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchLocation() {
googleRepository.fetch { sheet: Sheets.Spreadsheets.Values ->
val (map, marquee) = awaitAll(
async { sheet.get(Sheet.ID, Sheet.MAP).execute() },
async { sheet.get(Sheet.ID, Sheet.MARQUEE).execute() },
)
updateData(map = map, marquee = marquee)
}
}
@Throws(IncompatibleSheetStructure::class)
private fun updateData(map: ValueRange, marquee: ValueRange) {
val marquees = marqueeParser
.parse(data = marquee)
.groupBy { it.map }
val maps = locationParser
.parse(data = map)
.map {
val associatedMarquees = marquees[it.name]
if (associatedMarquees != null) {
it.copy(marquees = associatedMarquees)
} else {
it
}
}
_data.tryEmit(maps)
}
companion object {
private const val TAG = "LocationRepository"
}
private object Sheet {
const val ID = "1gbWaqXChz8pDJ-O3-Q8a_0wbDAYGlVA2voL7nEJUSn0"
const val MAP = "carte"
const val MARQUEE = "marqueur"
}
}

View file

@ -1,82 +1,35 @@
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.facotry.QuestParser
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 com.pixelized.rplexicon.utilitary.extentions.toUriOrNull
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 googleRepository: GoogleSheetServiceRepository,
private val questParser: QuestParser,
) {
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)
}
googleRepository.fetch { sheet ->
val request = sheet.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()
@Throws(IncompatibleSheetStructure::class, Exception::class)
private fun updateData(data: ValueRange) {
val questEntries = questParser.parse(value = data)
val questMap = questEntries.groupBy { it.title }
val quests = questMap.keys.mapIndexed { index, item ->
@ -86,77 +39,11 @@ class QuestRepository @Inject constructor(
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? String?
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
val background = row?.getOrNull(sheetStructure.background) as? String?
return if (title?.isNotEmpty() == true && description?.isNotEmpty() == true) {
QuestEntry(
sheetIndex = sheetIndex,
title = title,
subtitle = subtitle?.takeIf { it.isNotBlank() },
complete = complete.equals("TRUE", ignoreCase = true),
questGiver = questGiver?.takeIf { it.isNotBlank() },
area = area?.takeIf { it.isNotBlank() },
groupReward = groupReward?.takeIf { it.isNotBlank() },
individualReward = individualReward?.takeIf { it.isNotBlank() },
description = description,
background = background?.toUriOrNull(),
)
} 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 val Map<String, Int>?.background: Int get() = this?.getValue(Sheet.BACKGROUND) ?: 8
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",
"fond"
)
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]
val BACKGROUND = COLUMNS[8]
}
}