Add realtime database to the APP.

This commit is contained in:
Thomas Andres Gomez 2023-10-04 19:21:10 +02:00
parent e2209bf005
commit ee81f9082d
50 changed files with 1478 additions and 342 deletions

View file

@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M367.84,23.56c-1.25,-0.01 -2.5,0 -3.75,0.03 -39.56,1.01 -80.22,21.15 -108.66,61.94 -35.39,-50.06 -91.96,-68.93 -140.94,-58.9C61.6,37.46 17.57,83.13 19.84,154.19c2.38,74.37 56.07,131.62 109.84,179.97 26.89,24.17 54.03,46.28 75.47,67 21.43,20.72 36.76,40.13 41.31,57.13l9.03,33.69 9,-33.69c3.99,-14.88 19.58,-34.12 41.59,-55.03 22.01,-20.91 50,-43.8 77.47,-68.78 54.94,-49.96 109.17,-108.88 106.9,-180.88 -2.19,-69.7 -45.2,-115.58 -96.75,-127.13 -8.46,-1.9 -17.13,-2.87 -25.88,-2.91zM139.87,42.69c39.62,-0.27 81.75,20.09 107.53,64.75l7.75,13.44L255.16,239.75c-1.48,-0.18 -2.97,-0.28 -4.5,-0.28 -20.71,0 -37.53,16.78 -37.53,37.5 0,20.72 16.82,37.5 37.53,37.5 1.53,0 3.02,-0.1 4.5,-0.28v117.66c-8.78,-15.03 -21.8,-29.43 -37,-44.13 -1.05,-1.01 -2.12,-2.05 -3.19,-3.06 13.68,-2.09 24.19,-13.89 24.19,-28.16 0,-15.74 -12.79,-28.5 -28.53,-28.5 -15.3,0 -27.78,12.05 -28.47,27.19 -13.03,-11.32 -26.6,-22.92 -39.97,-34.94 -52.87,-47.54 -101.56,-101.07 -103.65,-166.66 -2.02,-63.34 34.62,-99.42 79.72,-108.66 7.05,-1.44 14.29,-2.2 21.63,-2.25zM180.62,84.44c-20.71,0 -37.5,16.78 -37.5,37.5 0,4.62 0.85,9.04 2.38,13.12 -0.14,-0 -0.27,0 -0.41,0 -12.15,0 -22,9.85 -22,22 0,12.15 9.85,22 22,22 12.15,0 22,-9.85 22,-22v-0.13c4.2,1.62 8.76,2.5 13.53,2.5 20.72,0 37.5,-16.78 37.5,-37.5 0,-20.72 -16.78,-37.5 -37.5,-37.5zM206.94,168.13c-10.83,0.55 -19.44,9.53 -19.44,20.5 0,11.32 9.18,20.5 20.5,20.5s20.5,-9.18 20.5,-20.5 -9.18,-20.5 -20.5,-20.5c-0.35,0 -0.71,-0.02 -1.06,0zM173.09,220.56c-14.36,0 -26,11.64 -26,26 0,14.36 11.64,26.03 26,26.03s26.03,-11.67 26.03,-26.03c0,-14.36 -11.67,-26 -26.03,-26z"/>
</vector>

View file

@ -6,20 +6,21 @@ data class CharacterSheet(
val proficiency: Int, // Bonus de maîtrise
val level: Int, // Niveau
val characterClass: String, // Classe
val hitPoint: String, // Point de vie
val maxHitPoint: String, // Point de vie MAX
val lifeDice: Int, // Dé de vie
val spell1: Counter?,
val spell2: Counter?,
val spell3: Counter?,
val spell4: Counter?,
val spell5: Counter?,
val spell6: Counter?,
val spell7: Counter?,
val spell8: Counter?,
val spell9: Counter?,
val hitPoint: Int, // Point de vie MAX
val rage: Int?,
val relentlessEndurance: Int?,
val divineConduit: Int?,
val bardicInspiration: Int?,
val spell1: Int?,
val spell2: Int?,
val spell3: Int?,
val spell4: Int?,
val spell5: Int?,
val spell6: Int?,
val spell7: Int?,
val spell8: Int?,
val spell9: Int?,
val dC: Int?,
val criticalModifier: Int, // Critical Dice Multiplier
val armorClass: Int, // Classe d'armure
val speed: Int, // Vitesse
val strength: Int, // Force

View file

@ -0,0 +1,80 @@
package com.pixelized.rplexicon.model
import com.google.firebase.database.PropertyName
data class CharacterSheetFire(
@get:PropertyName(HIT_POINT)
@set:PropertyName(HIT_POINT)
var hitPoint: HitPoint? = null,
@get:PropertyName(RAGE)
@set:PropertyName(RAGE)
var rage: Int? = null,
@get:PropertyName(RELENTLESS_ENDURANCE)
@set:PropertyName(RELENTLESS_ENDURANCE)
var relentlessEndurance: Int? = null,
@get:PropertyName(DIVINE_CONDUIT)
@set:PropertyName(DIVINE_CONDUIT)
var divineConduit: Int? = null,
@get:PropertyName(BARDIC_INSPIRATION)
@set:PropertyName(BARDIC_INSPIRATION)
var bardicInspiration: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}1")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}1")
var spell1: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}2")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}2")
var spell2: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}3")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}3")
var spell3: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}4")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}4")
var spell4: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}5")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}5")
var spell5: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}6")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}6")
var spell6: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}7")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}7")
var spell7: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}8")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}8")
var spell8: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}9")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}9")
var spell9: Int? = null,
) {
data class HitPoint(
@get:PropertyName("additional")
@set:PropertyName("additional")
var additional: Int? = null,
@get:PropertyName("value")
@set:PropertyName("value")
var value: Int? = null,
)
companion object {
const val HIT_POINT = "hit_point"
const val RAGE = "rage"
const val RELENTLESS_ENDURANCE = "relentless_endurance"
const val DIVINE_CONDUIT = "divine_conduit"
const val BARDIC_INSPIRATION = "bardic_inspiration"
const val SPELL_SLOT_LEVEL_X = "spell_slot_level_"
}
}

View file

@ -4,6 +4,7 @@ import android.accounts.Account
import android.content.Context
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import com.google.android.gms.auth.api.identity.SignInCredential
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
@ -17,32 +18,30 @@ import javax.inject.Singleton
class AuthenticationRepository @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val signInCredential = mutableStateOf<SignInCredential?>(null)
val isAuthenticated: State<Boolean> = derivedStateOf { signInCredential.value != null }
val credential: GoogleAccountCredential
get() {
val credential = GoogleAccountCredential
.usingOAuth2(context, capabilities)
.setBackOff(ExponentialBackOff())
credential.selectedAccount = signInCredential.value?.let {
Account(it.id, ACCOUNT_TYPE)
}
return credential
}
// private val signInCredential = mutableStateOf<SignInCredential?>(null)
// val isAuthenticated: State<Boolean> = derivedStateOf { signInCredential.value != null }
//
// val credential: GoogleAccountCredential by derivedStateOf {
// GoogleAccountCredential
// .usingOAuth2(context, capabilities)
// .setBackOff(ExponentialBackOff())
// .also {
// it.selectedAccount = signInCredential.value?.let {
// Account(it.id, ACCOUNT_TYPE)
// }
// }
// }
fun updateAuthenticationState(
credential: SignInCredential? = null,
) {
signInCredential.value = credential
// signInCredential.value = credential
}
companion object {
private const val ACCOUNT_TYPE = "google"
private val capabilities = listOf(
SheetsScopes.SPREADSHEETS_READONLY,
)
// private const val ACCOUNT_TYPE = "google"
// private val capabilities = listOf(
// SheetsScopes.SPREADSHEETS_READONLY,
// )
}
}

View file

@ -0,0 +1,89 @@
package com.pixelized.rplexicon.repository.authentication
import android.app.Application
import android.util.Log
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.CharacterSheetFire
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.callbackFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FirebaseRepository @Inject constructor(
application: Application,
) {
private val database = Firebase.database(
url = application.getString(R.string.firebase_realtime_database),
)
private val _error = MutableSharedFlow<Exception>()
val error: SharedFlow<Exception> get() = _error
init {
Firebase.database.setPersistenceEnabled(true)
}
fun getCharacter(character: String): Flow<CharacterSheetFire> {
return callbackFlow {
// reference to the node
val reference = database.getReference("$PATH_CHARACTERS/$character")
// build a register the callback
val listener = reference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val value = try {
dataSnapshot.getValue(CharacterSheetFire::class.java)
} catch (exception: Exception) {
_error.tryEmit(exception)
null
}
if (value != null) {
trySend(value)
}
}
override fun onCancelled(error: DatabaseError) {
Log.w(TAG, "Failed to read value.", error.toException())
cancel()
}
})
awaitClose {
reference.removeEventListener(listener)
}
}
}
fun setCharacterHitPoint(character: String, value: Int, extra: Int) {
val reference = database.getReference(
"$PATH_CHARACTERS/$character/${CharacterSheetFire.HIT_POINT}"
)
reference.setValue(
CharacterSheetFire.HitPoint(
additional = extra,
value = value,
)
)
}
fun setToken(character: String, token: String, value: Int) {
val reference = database.getReference(
"$PATH_CHARACTERS/$character/$token"
)
reference.setValue(value)
}
companion object {
private const val TAG = "FirebaseRepository"
private const val PATH_CHARACTERS = "Characters"
}
}

View file

@ -26,22 +26,55 @@ class AlterationRepository @Inject constructor(
var lastSuccessFullUpdate: Update = Update.INITIAL
private set
/**
* get all [Alteration] for a character
* @return a list of alterations.
*/
fun getAlterations(character: String): List<Alteration> {
return _assignedAlterations.value[character] ?: emptyList()
}
/**
* get [Alteration] for a character that affect at least one [Property]
* @param character the character name
* @param properties the property list to filter on.
* @return a list of alterations.
*/
fun getAlterations(character: String, vararg properties: Property): List<Alteration> {
return getAlterations(character = character).filter {
it.status.keys.any { key -> properties.contains(key) }
}
}
/**
* check the activation state of a given alteration for given character.
* @param character the character name
* @param alteration the alteration name
* @return true if the alteration is activated otherwise false
*/
fun getStatus(character: String, alteration: String): Boolean {
return _status.value[character + alteration]
?: _assignedAlterations.value[character]?.firstOrNull { it.name == alteration }?.active
?: false
}
/**
* get a map of [Property] and [Alteration.Status] for a given player if the alteration is active.
* @param character the character name
* @return a map of [Property] and [Alteration.Status]
*/
fun getActiveAlterationsStatus(character: String): Map<Property, List<Alteration.Status>> {
val status = hashMapOf<Property, MutableList<Alteration.Status>>()
_assignedAlterations.value[character]?.forEach { alteration ->
if (_status.value[character + alteration.name] ?: alteration.active) {
alteration.status.forEach {
status.getOrPut(it.key) { mutableListOf() }.add(it.value)
}
}
}
return status
}
suspend fun setStatus(character: String, alteration: String, value: Boolean?) {
_status.emit(if (value != null) {
_status.value.toMutableMap().also { it[character + alteration] = value }

View file

@ -7,6 +7,7 @@ import com.pixelized.rplexicon.utilitary.Update
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -2,16 +2,13 @@ package com.pixelized.rplexicon.repository.parser
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.repository.parser.alteration.CounterParser
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class CharacterSheetParser @Inject constructor(
private val counterParser: CounterParser,
) {
class CharacterSheetParser @Inject constructor() {
@Throws(IncompatibleSheetStructure::class)
fun parse(value: ValueRange): Map<String, CharacterSheet> {
@ -51,38 +48,21 @@ class CharacterSheetParser @Inject constructor(
proficiency = item.parseInt(MASTERY) ?: 2,
level = item.parseInt(LEVEL) ?: 2,
characterClass = item.parseString(CLASS) ?: "",
hitPoint = item.parseString(HIT_POINT) ?: "1",
maxHitPoint = item.parseString(MAX_HIT_POINT) ?: "1",
lifeDice = item.parseInt(LIFE_DICE) ?: 0,
spell1 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_1)
),
spell2 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_2)
),
spell3 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_3)
),
spell4 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_4)
),
spell5 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_5)
),
spell6 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_6)
),
spell7 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_7)
),
spell8 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_8)
),
spell9 = counterParser.parseCounter(
value = item.parseString(SPELL_LEVEL_9)
),
hitPoint = item.parseInt(MAX_HIT_POINT) ?: 1,
rage = item.parseInt(RAGE),
relentlessEndurance = item.parseInt(RELENTLESS_ENDURANCE),
divineConduit = item.parseInt(DIVINE_CONDUIT),
bardicInspiration = item.parseInt(BARDIC_INSPIRATION),
spell1 = item.parseInt(SPELL_LEVEL_1),
spell2 = item.parseInt(SPELL_LEVEL_2),
spell3 = item.parseInt(SPELL_LEVEL_3),
spell4 = item.parseInt(SPELL_LEVEL_4),
spell5 = item.parseInt(SPELL_LEVEL_5),
spell6 = item.parseInt(SPELL_LEVEL_6),
spell7 = item.parseInt(SPELL_LEVEL_7),
spell8 = item.parseInt(SPELL_LEVEL_8),
spell9 = item.parseInt(SPELL_LEVEL_9),
dC = item.parseInt(DD_SAVE_THROW),
criticalModifier = item.parseInt(CRITICAL_MODIFIER) ?: 2,
armorClass = item.parseInt(ARMOR_CLASS) ?: 10,
speed = item.parseInt(SPEED) ?: 10,
strength = item.parseInt(STRENGTH) ?: 10,
@ -131,9 +111,11 @@ class CharacterSheetParser @Inject constructor(
private const val RACE = "Race"
private const val LEVEL = "Niveau"
private const val CLASS = "Classe"
private const val HIT_POINT = "Point de vie"
private const val MAX_HIT_POINT = "Point de vie MAX"
private const val LIFE_DICE = "Dé de vie"
private const val RAGE = "Rage"
private const val RELENTLESS_ENDURANCE = "Endurance implacable"
private const val DIVINE_CONDUIT = "Conduit divin"
private const val BARDIC_INSPIRATION = "Inspiration bardique"
private const val SPELL_LEVEL_1 = "Sort de niveau 1"
private const val SPELL_LEVEL_2 = "Sort de niveau 2"
private const val SPELL_LEVEL_3 = "Sort de niveau 3"
@ -144,7 +126,6 @@ class CharacterSheetParser @Inject constructor(
private const val SPELL_LEVEL_8 = "Sort de niveau 8"
private const val SPELL_LEVEL_9 = "Sort de niveau 9"
private const val DD_SAVE_THROW = "DD sauvergarde des sorts"
private const val CRITICAL_MODIFIER = "Dé de critique"
private const val ARMOR_CLASS = "Classe d'armure"
private const val SPEED = "Vitesse"
private const val MASTERY = "Bonus de maîtrise"
@ -185,9 +166,11 @@ class CharacterSheetParser @Inject constructor(
RACE,
LEVEL,
CLASS,
HIT_POINT,
MAX_HIT_POINT,
LIFE_DICE,
RAGE,
RELENTLESS_ENDURANCE,
DIVINE_CONDUIT,
BARDIC_INSPIRATION,
SPELL_LEVEL_1,
SPELL_LEVEL_2,
SPELL_LEVEL_3,
@ -198,7 +181,6 @@ class CharacterSheetParser @Inject constructor(
SPELL_LEVEL_8,
SPELL_LEVEL_9,
DD_SAVE_THROW,
CRITICAL_MODIFIER,
ARMOR_CLASS,
SPEED,
MASTERY,

View file

@ -116,7 +116,7 @@ class IndicatorStyle(
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun IndicatorStepPreview() {
fun IndicatorStepPreview() {
LexiconTheme {
Surface {
IndicatorStep(

View file

@ -0,0 +1,83 @@
package com.pixelized.rplexicon.ui.composable
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlin.math.absoluteValue
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NumberPicker(
modifier: Modifier = Modifier,
itemSize: Dp = 42.dp,
itemAmount: Int = 5,
from: Int = 0,
pager: PagerState,
) {
VerticalPager(
modifier = Modifier
.height(height = itemSize * itemAmount)
.then(other = modifier),
state = pager,
contentPadding = PaddingValues(vertical = itemSize * itemAmount.floorDiv(2)),
pageSize = PageSize.Fixed(itemSize),
) { index ->
Box(
modifier = Modifier.size(size = itemSize),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.graphicsLayer {
val fraction = 1f -
((pager.currentPage - index) + pager.currentPageOffsetFraction)
.absoluteValue
.coerceIn(0f, 1f)
lerp(start = 0.5f, stop = 1f, fraction = fraction).let {
scaleX = it
scaleY = it
}
lerp(start = 0.35f, stop = 1f, fraction = fraction).let {
alpha = it
}
},
style = MaterialTheme.typography.headlineLarge,
text = "${index + from}",
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun NumberPickerPreview() {
LexiconTheme {
Surface {
NumberPicker(
pager = rememberPagerState(initialPage = 20) { 99 }
)
}
}
}

View file

@ -0,0 +1,84 @@
package com.pixelized.rplexicon.ui.composable.edit
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.rplexicon.ui.composable.NumberPicker
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
data class CounterEditDialogUio(
val id: String,
val label: String,
val value: Int,
val max: Int,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HandleCounterEditDialog(
dialog: State<CounterEditDialogUio?>,
onDismissRequest: () -> Unit,
onConfirm: (String, Int) -> Unit,
) {
dialog.value?.let {
val pager = rememberPagerState(initialPage = it.value) { it.max + 1 }
Dialog(
properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
onDismissRequest = onDismissRequest,
) {
Surface(
modifier = Modifier.ddBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
style = MaterialTheme.typography.labelSmall,
text = it.label,
)
Box(
modifier = Modifier
.size(width = 64.dp, height = 1.dp)
.background(color = MaterialTheme.lexicon.colorScheme.characterSheet.innerBorder),
)
NumberPicker(
modifier = Modifier.width(width = 128.dp),
pager = pager,
)
TextButton(
onClick = { onConfirm(it.id, pager.currentPage) },
) {
Text(text = stringResource(id = android.R.string.ok))
}
}
}
}
}
}

View file

@ -0,0 +1,129 @@
package com.pixelized.rplexicon.ui.composable.edit
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.NumberPicker
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
data class HpPointDialogUio(
val value: Int,
val max: Int,
val extra: Int,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HandleHitPointEditDialog(
dialog: State<HpPointDialogUio?>,
onDismissRequest: () -> Unit,
onConfirm: (hp: Int, extra: Int) -> Unit,
) {
dialog.value?.let {
val hpPager = rememberPagerState(initialPage = it.value) { it.max + 1 }
val extraPager = rememberPagerState(initialPage = it.extra) { 99 }
Dialog(
properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
onDismissRequest = onDismissRequest,
) {
Surface(
modifier = Modifier.ddBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
text = stringResource(id = R.string.character_sheet_title_hp),
)
Box(
modifier = Modifier
.size(width = 64.dp, height = 1.dp)
.background(color = MaterialTheme.lexicon.colorScheme.characterSheet.innerBorder),
)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
NumberPicker(
modifier = Modifier.width(width = 64.dp),
pager = hpPager,
)
Text(
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.bodyLarge,
text = "+"
)
NumberPicker(
modifier = Modifier.width(width = 64.dp),
pager = extraPager,
)
}
TextButton(
onClick = { onConfirm(hpPager.currentPage, extraPager.currentPage) },
) {
Text(text = stringResource(id = android.R.string.ok))
}
}
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun HandleHitPointEditDialogPreview() {
LexiconTheme {
Surface {
val dialog = remember {
mutableStateOf(
HpPointDialogUio(
value = 20,
max = 30,
extra = 0,
)
)
}
HandleHitPointEditDialog(
dialog = dialog,
onDismissRequest = { },
onConfirm = { _, _ -> },
)
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE
import com.pixelized.rplexicon.ui.navigation.screens.HOME_ROUTE
import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication
import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterSheet
@ -26,7 +27,7 @@ val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> {
@Composable
fun ScreenNavHost(
navHostController: NavHostController = rememberNavController(),
startDestination: String = HOME_ROUTE,
startDestination: String = AUTHENTICATION_ROUTE,
) {
val lexiconListState = rememberLazyListState()
val questListState = rememberLazyListState()

View file

@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
@ -49,6 +50,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalActivity
import com.pixelized.rplexicon.LocalSnack
@ -70,6 +72,9 @@ sealed class AuthenticationStateUio {
@Stable
data object Initial : AuthenticationStateUio()
@Stable
data object Progress : AuthenticationStateUio()
@Stable
data object Success : AuthenticationStateUio()
@ -104,6 +109,11 @@ fun AuthenticationScreen(
HandleAuthenticationState(
state = state,
onProgress = {
Dialog(onDismissRequest = { }) {
CircularProgressIndicator()
}
},
onSignIn = {
screen.navigateToHome(option = rootOption())
},
@ -119,20 +129,24 @@ fun AuthenticationScreen(
@Composable
fun HandleAuthenticationState(
state: State<AuthenticationStateUio>,
onProgress: @Composable () -> Unit,
onSignIn: suspend CoroutineScope.() -> Unit,
onSignInError: suspend CoroutineScope.(exception: Exception?) -> Unit,
) {
when (val dummy = state.value) {
AuthenticationStateUio.Initial -> Unit
is AuthenticationStateUio.Success -> {
LaunchedEffect(key1 = "Authentication.Success", block = onSignIn)
}
is AuthenticationStateUio.Initial -> Unit
is AuthenticationStateUio.Failure -> {
LaunchedEffect(key1 = "Authentication.Error") {
onSignInError(dummy.exception)
}
}
is AuthenticationStateUio.Progress -> onProgress()
is AuthenticationStateUio.Success -> LaunchedEffect(
key1 = "Authentication.Success",
block = onSignIn,
)
is AuthenticationStateUio.Failure -> LaunchedEffect(
key1 = "Authentication.Error",
block = { onSignInError(dummy.exception) },
)
}
}

View file

@ -16,6 +16,9 @@ import androidx.lifecycle.AndroidViewModel
import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.auth.api.identity.SignInCredential
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.repository.authentication.AuthenticationRepository
import dagger.hilt.android.lifecycle.HiltViewModel
@ -30,11 +33,12 @@ class AuthenticationViewModel @Inject constructor(
private val context: Context get() = getApplication()
private var launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>? = null
private val state = mutableStateOf(
when (repository.isAuthenticated.value) {
true -> AuthenticationStateUio.Success
else -> AuthenticationStateUio.Initial
}
private val state = mutableStateOf<AuthenticationStateUio>(
AuthenticationStateUio.Initial
// when (repository.isAuthenticated.value) {
// true -> AuthenticationStateUio.Success
// else -> AuthenticationStateUio.Initial
// }
)
@Composable
@ -43,13 +47,28 @@ class AuthenticationViewModel @Inject constructor(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = {
if (it.resultCode == Activity.RESULT_OK) {
val credential: SignInCredential = Identity
state.value = AuthenticationStateUio.Progress
// sign in request succeed. retrieve google credential
val googleCredential: SignInCredential = Identity
.getSignInClient(context)
.getSignInCredentialFromIntent(it.data)
state.value = AuthenticationStateUio.Success
repository.updateAuthenticationState(credential = credential)
// build firebase credential
val firebaseCredential = GoogleAuthProvider
.getCredential(googleCredential.googleIdToken, null)
// sign in to Firebase
Firebase.auth
.signInWithCredential(firebaseCredential)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
state.value = AuthenticationStateUio.Success
repository.updateAuthenticationState(credential = googleCredential)
} else {
state.value = AuthenticationStateUio.Failure(task.exception)
repository.updateAuthenticationState(credential = null)
}
}
} else {
state.value = AuthenticationStateUio.Initial
repository.updateAuthenticationState(credential = null)
}
},
@ -60,10 +79,13 @@ class AuthenticationViewModel @Inject constructor(
fun signIn(activity: Activity) {
state.value = AuthenticationStateUio.Initial
// build a request to sign in with google credential.
// At that point we do only use google sign in service
val request: GetSignInIntentRequest = GetSignInIntentRequest.builder()
.setServerClientId(context.getString(R.string.google_sign_in_id))
.build()
// use the pre register launcher to start the sign in request intent.
Identity.getSignInClient(activity).getSignInIntent(request)
.addOnSuccessListener { result ->
try {

View file

@ -38,21 +38,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.zIndex
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalRollOverlay
import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.IndicatorStep
import com.pixelized.rplexicon.ui.composable.IndicatorStepPreview
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderPreview
import com.pixelized.rplexicon.ui.screens.character.composable.character.ProficiencyUio.ID.*
import com.pixelized.rplexicon.ui.screens.character.composable.character.StatUio.ID.*
import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPage
@ -114,11 +115,10 @@ fun CharacterSheetScreen(
onBack = {
screen.popBackStack()
},
header = {
CharacterSheetHeader(
modifier = Modifier.zIndex(1f),
pagerState = pagerState,
header = viewModel.header,
indicator = {
IndicatorStep(
count = pagerState.pageCount,
selectedIndex = pagerState.currentPage,
)
},
loader = {
@ -135,7 +135,6 @@ fun CharacterSheetScreen(
},
actions = {
ActionPage(
character = viewModel.character,
sheetState = sheetState,
attackViewModel = attackViewModel,
spellsViewModel = spellsViewModel,
@ -182,11 +181,11 @@ private fun CharacterSheetContent(
refreshState: PullRefreshState,
onRefresh: () -> Unit,
onBack: () -> Unit,
header: @Composable ColumnScope.() -> Unit,
loader: @Composable BoxScope.() -> Unit,
proficiencies: @Composable PagerScope.() -> Unit,
actions: @Composable PagerScope.() -> Unit,
alterations: @Composable PagerScope.() -> Unit,
indicator: @Composable ColumnScope.() -> Unit,
sheet: @Composable () -> Unit,
) {
Scaffold(
@ -195,6 +194,7 @@ private fun CharacterSheetContent(
contentWindowInsets = NO_WINDOW_INSETS,
topBar = {
TopAppBar(
modifier = Modifier.shadow(elevation = 4.dp),
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
@ -232,9 +232,8 @@ private fun CharacterSheetContent(
content = {
Column(
modifier = Modifier.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
header()
Box(
modifier = Modifier
.fillMaxWidth()
@ -255,9 +254,9 @@ private fun CharacterSheetContent(
}
}
)
loader()
}
indicator()
}
}
)
@ -288,11 +287,11 @@ private fun CharacterScreenPreview(
refreshState = rememberPullRefreshState(refreshing = false, onRefresh = { }),
onRefresh = { },
onBack = { },
header = { CharacterSheetHeaderPreview() },
loader = { },
proficiencies = { ProficiencyPreview() },
actions = { ActionPagePreview() },
alterations = { AlterationPagePreview() },
indicator = { IndicatorStepPreview() },
sheet = { SpellLevelChooserPreview() },
)
}

View file

@ -3,60 +3,31 @@ package com.pixelized.rplexicon.ui.screens.character
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.repository.data.ActionRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.repository.data.SpellRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.factory.CharacterSheetHeaderUioFactory
import com.pixelized.rplexicon.utilitary.extentions.local.toActiveStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class CharacterSheetViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val characterRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
private val actionRepository: ActionRepository,
private val spellRepository: SpellRepository,
headerFactory: CharacterSheetHeaderUioFactory,
) : ViewModel() {
val character = savedStateHandle.characterSheetArgument.name
private val _header = mutableStateOf<CharacterSheetHeaderUio?>(null)
val header: State<CharacterSheetHeaderUio?> get() = _header
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
init {
viewModelScope.launch {
launch {
characterRepository.data
.combine(alterationRepository.assignedAlterations) { sheets, _ -> sheets }
.collect { sheets ->
_header.value = withContext(Dispatchers.Default) {
val alterations =
alterationRepository.getAlterations(character = character)
headerFactory.convert(
model = sheets.getValue(character),
alterations = alterations.toActiveStatus(),
)
}
}
}
launch {
update(force = false)
}

View file

@ -6,6 +6,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -50,12 +51,15 @@ data class AttackUio(
@Composable
fun Attack(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
weapon: AttackUio,
onHit: (String) -> Unit,
onDamage: (String) -> Unit,
) {
Row(
modifier = modifier,
modifier = Modifier
.padding(paddingValues = padding)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@ -116,9 +120,7 @@ private fun WeaponPreview(
LexiconTheme {
Surface {
Attack(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
modifier = Modifier.fillMaxWidth(),
weapon = preview,
onHit = { },
onDamage = { },

View file

@ -3,8 +3,10 @@ package com.pixelized.rplexicon.ui.screens.character.composable.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@ -37,24 +39,30 @@ data class SpellHeaderUio(
@Stable
data class Count(
val value: Int,
val max: Int?,
val max: Int,
)
}
@Composable
fun SpellHeader(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(top = 8.dp, bottom = 4.dp),
header: SpellHeaderUio,
onSpell: (level: Int, value: Int, max: Int) -> Unit,
) {
Box(
modifier = modifier
.background(color = MaterialTheme.colorScheme.surface)
.clickable(enabled = header.count != null) {
header.count?.let { onSpell(header.level, it.value, it.max) }
}
.padding(horizontal = 16.dp)
.heightIn(min = 32.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues = padding)
.align(alignment = Alignment.Center),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
@ -88,23 +96,22 @@ fun SpellHeader(
header.count?.let { count ->
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
text = "${count.value}",
)
count.max?.let { max ->
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.labelSmall,
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.bodySmall,
text = "$max",
)
}
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.labelSmall,
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.bodySmall,
text = "${count.max}",
)
}
}
@ -125,6 +132,7 @@ private fun SpellHeaderPreview(
Surface {
SpellHeader(
header = preview,
onSpell = { _, _, _ -> },
)
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -58,6 +59,7 @@ data class SpellUio(
@Composable
fun Spell(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
spell: SpellUio,
onClick: (String) -> Unit,
onHit: (String) -> Unit,
@ -67,6 +69,7 @@ fun Spell(
Row(
modifier = Modifier
.clickable { onClick(spell.name) }
.padding(paddingValues = padding)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 12.dp),
verticalAlignment = Alignment.CenterVertically,

View file

@ -2,14 +2,12 @@ package com.pixelized.rplexicon.ui.screens.character.composable.character
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@ -19,9 +17,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.composable.IndicatorStep
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
data class CharacterSheetHeaderUio(
@ -31,55 +29,58 @@ data class CharacterSheetHeaderUio(
val dC: LabelPointUio?,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CharacterSheetHeader(
modifier: Modifier = Modifier,
pagerState: PagerState,
padding : PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
header: State<CharacterSheetHeaderUio?>,
onHitPoint : () -> Unit,
) {
Surface(
modifier = modifier,
shadowElevation = 4.dp,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
modifier = Modifier.padding(padding),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterHorizontally
),
) {
header.value?.armorClass?.let { LabelPoint(label = it) }
header.value?.hitPoint?.let { LabelPoint(label = it) }
header.value?.dC?.let { LabelPoint(label = it) }
header.value?.speed?.let { LabelPoint(label = it,) }
header.value?.armorClass?.let {
LabelPoint(label = it)
}
header.value?.hitPoint?.let {
LabelPoint(label = it, onClick = onHitPoint)
}
header.value?.dC?.let {
LabelPoint(label = it)
}
header.value?.speed?.let {
LabelPoint(label = it)
}
}
IndicatorStep(
modifier = Modifier.padding(vertical = 4.dp),
count = pagerState.pageCount,
selectedIndex = pagerState.currentPage,
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.lexicon.colorScheme.placeholder,
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
fun CharacterSheetHeaderPreview() {
private fun CharacterSheetHeaderPreview() {
LexiconTheme {
Surface {
CharacterSheetHeader(
header = rememberCharacterHeaderStatePreview(),
pagerState = rememberPagerState { 2 },
onHitPoint = { },
)
}
}

View file

@ -2,15 +2,19 @@ package com.pixelized.rplexicon.ui.screens.character.composable.character
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -22,7 +26,7 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
class LabelPointUio(
data class LabelPointUio(
val label: Int?,
val value: String?,
val max: String? = null,
@ -35,9 +39,16 @@ fun LabelPoint(
valueStyle: TextStyle = MaterialTheme.typography.headlineMedium,
maxStyle: TextStyle = MaterialTheme.typography.titleMedium,
label: LabelPointUio,
onClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
enabled = onClick != null, onClick = { onClick?.invoke() }
)
.then(other = modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
label.label?.let {
@ -53,6 +64,10 @@ fun LabelPoint(
Text(
modifier = Modifier.alignByBaseline(),
style = valueStyle,
color = when (onClick) {
null -> MaterialTheme.colorScheme.onSurface
else -> MaterialTheme.colorScheme.primary
},
fontWeight = FontWeight.Bold,
text = label.value ?: "0"
)
@ -91,6 +106,7 @@ private fun LabelPointPreview() {
value = "13",
max = "/ 15"
),
onClick = { },
)
LabelPoint(
label = LabelPointUio(

View file

@ -1,42 +1,34 @@
package com.pixelized.rplexicon.ui.screens.character.factory
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Alteration
import com.pixelized.rplexicon.model.Attack
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.character.LabelPointUio
import com.pixelized.rplexicon.ui.screens.character.pages.actions.HeaderViewModel
import javax.inject.Inject
class CharacterSheetHeaderUioFactory @Inject constructor() {
fun convert(
model: CharacterSheet,
alterations: Map<Property, List<Alteration.Status>>,
sheetHeaderData: HeaderViewModel.SheetHeaderData?,
fireHeaderData: HeaderViewModel.FireHeaderData?,
): CharacterSheetHeaderUio {
// compute alteration for the CA.
val armorClassAlteration: Int = alterations[Property.ARMOR_CLASS]?.sumOf {
it.bonus.sumOf { bonus -> bonus.value }
} ?: 0
return CharacterSheetHeaderUio(
armorClass = LabelPointUio(
label = R.string.character_sheet_title_ca,
value = "${model.armorClass + armorClassAlteration}",
value = sheetHeaderData?.ca?.let { "$it" } ?: " ",
max = null,
),
speed = LabelPointUio(
label = R.string.character_sheet_title_speed,
value = "${model.speed}",
value = sheetHeaderData?.speed?.let { "$it" } ?: " ",
max = "m",
),
hitPoint = LabelPointUio(
label = R.string.character_sheet_title_hp,
value = model.hitPoint,
max = "/ ${model.maxHitPoint}",
value = convertToHitPointLabel(hitPoint = fireHeaderData),
max = sheetHeaderData?.hpMax?.let { "/ $it" } ?: " ",
),
dC = model.dC?.let {
dC = sheetHeaderData?.dc?.let {
LabelPointUio(
label = R.string.character_sheet_title_dc,
value = "$it",
@ -44,4 +36,12 @@ class CharacterSheetHeaderUioFactory @Inject constructor() {
}
)
}
private fun convertToHitPointLabel(hitPoint: HeaderViewModel.FireHeaderData?): String {
return when {
hitPoint?.hp == null -> "?"
hitPoint.extra == 0 -> "${hitPoint.hp}"
else -> "${hitPoint.hp}+${hitPoint.extra}"
}
}
}

View file

@ -4,8 +4,10 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@ -13,15 +15,22 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalRollOverlay
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.edit.HandleCounterEditDialog
import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSpellDetail
import com.pixelized.rplexicon.ui.screens.character.composable.actions.Attack
@ -30,18 +39,26 @@ import com.pixelized.rplexicon.ui.screens.character.composable.actions.Spell
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeader
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellUio
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberAttackListStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberSpellListStatePreview
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItem
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItemUio
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.rememberTokenListStatePreview
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ActionPage(
character: String,
sheetState: ModalBottomSheetState,
headerViewModel: HeaderViewModel = hiltViewModel(),
attackViewModel: AttackActionViewModel = hiltViewModel(),
spellsViewModel: SpellsActionViewModel = hiltViewModel(),
tokenViewModel: TokenViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
val overlay = LocalRollOverlay.current
@ -49,8 +66,11 @@ fun ActionPage(
ActionsPageContent(
modifier = Modifier.fillMaxWidth(),
header = headerViewModel.header,
attacks = attackViewModel.attacks,
tokens = tokenViewModel.tokens,
spells = spellsViewModel.spells,
onHitPoint = headerViewModel::toggleHitPointDialog,
onAttackHit = { id ->
attackViewModel.onHitRoll(id)?.let {
overlay.prepareRoll(diceThrow = it)
@ -63,9 +83,15 @@ fun ActionPage(
overlay.showOverlay()
}
},
onToken = {
tokenViewModel.showTokenEditDialog(dialog = it)
},
onSpellLevel = { level: Int, value: Int, max: Int ->
tokenViewModel.showSpellTokenEditDialog(level = level, value = value, max = max)
},
onSpell = { spell ->
screen.navigateToSpellDetail(
character = character,
character = headerViewModel.character,
spell = spell,
)
},
@ -87,6 +113,18 @@ fun ActionPage(
}
},
)
HandleHitPointEditDialog(
dialog = headerViewModel.hitPointDialog,
onDismissRequest = headerViewModel::toggleHitPointDialog,
onConfirm = headerViewModel::applyHitPointChange,
)
HandleCounterEditDialog(
dialog = tokenViewModel.dialog,
onDismissRequest = tokenViewModel::hideCounterEditDialog,
onConfirm = tokenViewModel::applyCounterValue
)
}
@OptIn(ExperimentalFoundationApi::class)
@ -94,10 +132,15 @@ fun ActionPage(
fun ActionsPageContent(
modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(),
header: State<CharacterSheetHeaderUio?>,
attacks: State<List<AttackUio>>,
tokens: State<List<TokenItemUio>>,
spells: State<List<Pair<SpellHeaderUio, List<SpellUio>>>>,
onHitPoint: () -> Unit,
onAttackHit: (id: String) -> Unit,
onAttackDamage: (id: String) -> Unit,
onToken: (TokenItemUio) -> Unit,
onSpellLevel: (level: Int, value: Int, max: Int) -> Unit,
onSpell: (id: String) -> Unit,
onSpellHit: (id: String) -> Unit,
onSpellDamage: (id: String) -> Unit,
@ -106,28 +149,73 @@ fun ActionsPageContent(
LazyColumn(
modifier = modifier,
state = lazyListState,
contentPadding = PaddingValues(vertical = 8.dp),
contentPadding = PaddingValues(bottom = 8.dp),
) {
stickyHeader {
CharacterSheetHeader(
modifier = Modifier.fillMaxWidth(),
header = header,
onHitPoint = onHitPoint,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(items = attacks.value) {
Attack(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
weapon = it,
onHit = onAttackHit,
onDamage = onAttackDamage,
)
}
spells.value.forEach { entry ->
if (tokens.value.isNotEmpty()) {
items(count = 1) {
Spacer(modifier = Modifier.height(height = 16.dp))
}
stickyHeader {
SpellHeader(
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
header = entry.first,
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = MaterialTheme.typography.titleMedium,
text = stringResource(id = R.string.token_label_title).let {
AnnotatedString(
text = it,
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
start = 0,
end = Integer.min(1, it.length),
)
)
)
}
)
}
items(items = tokens.value) {
TokenItem(
counter = it,
onClick = onToken,
)
}
}
spells.value.forEach { entry ->
items(count = 1) {
Spacer(modifier = Modifier.height(height = 8.dp))
}
stickyHeader {
SpellHeader(
header = entry.first,
onSpell = onSpellLevel,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(items = entry.second) {
Spell(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
spell = it,
onClick = onSpell,
onHit = onSpellHit,
@ -147,10 +235,15 @@ fun ActionPagePreview() {
Surface {
ActionsPageContent(
modifier = Modifier.fillMaxSize(),
header = rememberCharacterHeaderStatePreview(),
attacks = rememberAttackListStatePreview(),
tokens = rememberTokenListStatePreview(),
spells = rememberSpellListStatePreview(),
onHitPoint = { },
onAttackHit = { },
onAttackDamage = { },
onToken = { },
onSpellLevel = { _, _, _ -> },
onSpell = { },
onSpellHit = { },
onSpellDamage = { },

View file

@ -15,9 +15,9 @@ import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AttackUio
import com.pixelized.rplexicon.ui.screens.character.factory.AttackUioFactory
import com.pixelized.rplexicon.utilitary.extentions.local.toActiveStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -40,18 +40,22 @@ class AttackActionViewModel @Inject constructor(
init {
viewModelScope.launch {
launch {
actionRepository.data.collect { sheets ->
_attacks.value = withContext(Dispatchers.Default) {
val alterations = alterationRepository.getAlterations(character = character)
sheets[character]?.map { action ->
attackFactory.convert(
characterSheet = model,
alterations = alterations.toActiveStatus(),
attack = action,
actionRepository.data
.combine(alterationRepository.assignedAlterations) { actions, _ -> actions }
.collect { sheets ->
_attacks.value = withContext(Dispatchers.Default) {
val alterations = alterationRepository.getActiveAlterationsStatus(
character = character,
)
} ?: emptyList()
sheets[character]?.map { action ->
attackFactory.convert(
characterSheet = model,
alterations = alterations,
attack = action,
)
} ?: emptyList()
}
}
}
}
}
}

View file

@ -0,0 +1,118 @@
package com.pixelized.rplexicon.ui.screens.character.pages.actions
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.composable.edit.HpPointDialogUio
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.factory.CharacterSheetHeaderUioFactory
import com.pixelized.rplexicon.utilitary.extentions.local.sum
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class HeaderViewModel @Inject constructor(
private val headerFactory: CharacterSheetHeaderUioFactory,
private val firebaseRepository: FirebaseRepository,
characterRepository: CharacterSheetRepository,
alterationRepository: AlterationRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
val character = savedStateHandle.characterSheetArgument.name
private val sheetData = mutableStateOf<SheetHeaderData?>(null)
private val fireData = mutableStateOf<FireHeaderData?>(null)
val header: State<CharacterSheetHeaderUio?> = derivedStateOf {
headerFactory.convert(
sheetHeaderData = sheetData.value,
fireHeaderData = fireData.value,
)
}
private val _hitPointDialog = mutableStateOf<HpPointDialogUio?>(null)
val hitPointDialog: State<HpPointDialogUio?> get() = _hitPointDialog
init {
viewModelScope.launch {
launch(context = Dispatchers.IO) {
characterRepository.data
.combine(alterationRepository.assignedAlterations) { sheets, _ -> sheets }
.collect { sheets ->
val character = sheets.getValue(character)
val alterations = alterationRepository.getActiveAlterationsStatus(
character = character.name,
)
val data = SheetHeaderData(
hpMax = character.hitPoint + alterations[Property.HIT_POINT].sum,
speed = character.speed,
ca = character.armorClass + alterations[Property.ARMOR_CLASS].sum,
dc = character.dC,
)
withContext(Dispatchers.Main) {
sheetData.value = data
}
}
}
launch(context = Dispatchers.IO) {
firebaseRepository.getCharacter(character = character).collect { sheets ->
val data = FireHeaderData(
hp = sheets.hitPoint?.value ?: 1,
extra = sheets.hitPoint?.additional ?: 0,
)
withContext(Dispatchers.Main) {
fireData.value = data
}
}
}
}
}
fun toggleHitPointDialog() {
_hitPointDialog.value = if (_hitPointDialog.value == null) {
HpPointDialogUio(
value = fireData.value?.hp ?: 10,
extra = fireData.value?.extra ?: 0,
max = sheetData.value?.hpMax ?: 10,
)
} else {
null
}
}
fun applyHitPointChange(hp: Int, extra: Int) {
firebaseRepository.setCharacterHitPoint(
character = character,
value = hp,
extra = extra,
)
_hitPointDialog.value = null
}
@Stable
data class SheetHeaderData(
val hpMax: Int,
val speed: Int,
val ca: Int,
val dc: Int?,
)
@Stable
data class FireHeaderData(
val hp: Int,
val extra: Int,
)
}

View file

@ -7,10 +7,13 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.AssignedSpell
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.CharacterSheetFire
import com.pixelized.rplexicon.model.DiceThrow
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.model.Throw
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.repository.data.SpellRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
@ -26,6 +29,7 @@ import com.pixelized.rplexicon.utilitary.extentions.local.spell
import com.pixelized.rplexicon.utilitary.extentions.modifier
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -33,14 +37,16 @@ import kotlin.math.max
@HiltViewModel
class SpellsActionViewModel @Inject constructor(
application: Application,
savedStateHandle: SavedStateHandle,
private val characterRepository: CharacterSheetRepository,
private val firebaseRepository: FirebaseRepository,
private val spellRepository: SpellRepository,
application: Application,
spellFactory: SpellUioFactory,
savedStateHandle: SavedStateHandle,
) : AndroidViewModel(application) {
private val character = savedStateHandle.characterSheetArgument.name
private val model: CharacterSheet get() = characterRepository.data.value.getValue(character)
private val characterName = savedStateHandle.characterSheetArgument.name
private val character: CharacterSheet
get() = characterRepository.data.value.getValue(characterName)
private val _spells = mutableStateOf<List<Pair<SpellHeaderUio, List<SpellUio>>>>(emptyList())
val spells: State<List<Pair<SpellHeaderUio, List<SpellUio>>>> get() = _spells
@ -51,95 +57,107 @@ class SpellsActionViewModel @Inject constructor(
init {
// TODO rework that part. use factory
viewModelScope.launch {
launch {
spellRepository.spells.collect {
_spells.value = withContext(Dispatchers.Default) {
if (model.isWarlock) {
it[character]
?.sortedBy { it.spell.name }
?.sortedBy { it.spell.level }
?.groupBy { it.spell.level == 0 }
?.map { entry ->
if (entry.key) {
SpellHeaderUio(
level = 0,
count = null,
)
} else {
val firstSpellSlot = model.firstSpellSlot()
SpellHeaderUio(
level = firstSpellSlot ?: 1,
count = model.spell(level = firstSpellSlot ?: 1)?.let {
SpellHeaderUio.Count(
it.value,
it.max
)
}
)
} to entry.value.map {
spellFactory.toUio(
assignedSpell = it,
characterSheet = model
)
}
}
?: emptyList()
} else {
it[character]
?.sortedBy { it.spell.name }
?.sortedBy { it.spell.level }
?.groupBy { it.spell.level }
?.map { entry ->
SpellHeaderUio(
level = entry.key,
count = model.spell(level = entry.key)?.let {
SpellHeaderUio.Count(
it.value,
it.max
launch(Dispatchers.IO) {
characterRepository.data
.combine(spellRepository.spells) { sheets, spells ->
Struct(sheets = sheets, spells = spells)
}
.combine(firebaseRepository.getCharacter(character = characterName)) { struct, fire ->
struct.also { it.fire = fire }
}
.collect { data ->
val spells = data.spells[characterName]
val character = data.sheets.getValue(characterName)
_spells.value = withContext(Dispatchers.Default) {
if (character.isWarlock) {
spells
?.sortedBy { it.spell.name }
?.sortedBy { it.spell.level }
?.groupBy { it.spell.level == 0 }
?.map { entry ->
if (entry.key) {
SpellHeaderUio(
level = 0,
count = null,
)
},
) to entry.value.map {
spellFactory.toUio(
assignedSpell = it,
characterSheet = model
)
} else {
val firstSpellSlot = character.firstSpellSlot()
SpellHeaderUio(
level = firstSpellSlot ?: 1,
count = character.spell(level = firstSpellSlot ?: 1)
?.let { max ->
SpellHeaderUio.Count(
value = data.fire.spell(
level = firstSpellSlot ?: 1
) ?: 0,
max = max
)
}
)
} to entry.value.map {
spellFactory.toUio(
assignedSpell = it,
characterSheet = character
)
}
}
}
?: emptyList()
?: emptyList()
} else {
spells
?.sortedBy { it.spell.name }
?.sortedBy { it.spell.level }
?.groupBy { it.spell.level }
?.map { entry ->
SpellHeaderUio(
level = entry.key,
count = character.spell(level = entry.key)?.let { max ->
SpellHeaderUio.Count(
value = data.fire.spell(level = entry.key) ?: 0,
max = max
)
},
) to entry.value.map {
spellFactory.toUio(
assignedSpell = it,
characterSheet = character
)
}
}
?: emptyList()
}
}
}
}
}
}
}
fun shouldDisplaySpellLevelChooser(name: String): Boolean {
val assignedSpell = spellRepository.find(
character = character,
character = characterName,
spell = name,
)
return when (model.isWarlock) {
return when (character.isWarlock) {
true -> false
else -> model.highestSpellLevel() > (assignedSpell?.spell?.level ?: 1)
else -> character.highestSpellLevel() > (assignedSpell?.spell?.level ?: 1)
}
}
fun prepareSpellCast(name: String) {
val assignedSpell = spellRepository.find(
character = character,
character = characterName,
spell = name,
)
if (assignedSpell != null) {
val icon = assignedSpell.effect?.faces?.icon ?: R.drawable.ic_d20_24
val base = assignedSpell.effect?.toString(character = model, level = 1) ?: ""
val base = assignedSpell.effect?.toString(character = character, level = 1) ?: ""
_preparedSpellLevel.value = SpellChooserUio(
name = name,
spells = List(
size = max(0, model.highestSpellLevel() + 1 - assignedSpell.spell.level)
size = max(0, character.highestSpellLevel() + 1 - assignedSpell.spell.level)
) { index ->
val level = assignedSpell.spell.level + index
val remaining = model.spell(level)?.value
val max = model.spell(level)?.max
val remaining = character.spell(level)
val max = character.spell(level)
SpellLevelUio(
spell = assignedSpell.spell.name,
@ -148,7 +166,7 @@ class SpellsActionViewModel @Inject constructor(
max = max,
icon = icon,
value = base + (assignedSpell.level
?.toString(character = model, level = index)
?.toString(character = character, level = index)
?.let { " + $it" }
?: "")
)
@ -158,26 +176,26 @@ class SpellsActionViewModel @Inject constructor(
}
fun onCastSpell(id: String): DiceThrow {
val spell = spellRepository.find(character = character, spell = id)
val spell = spellRepository.find(character = characterName, spell = id)
return onCastSpell(
id = id,
level = when (model.isWarlock) {
true -> model.firstSpellSlot() ?: 1
level = when (character.isWarlock) {
true -> character.firstSpellSlot() ?: 1
else -> spell?.spell?.level ?: 1
},
)
}
fun onCastSpell(id: String, level: Int): DiceThrow {
return DiceThrow.SpellEffect(character = character, spell = id, level = level)
return DiceThrow.SpellEffect(character = characterName, spell = id, level = level)
}
fun onSpellHitRoll(id: String): DiceThrow {
return DiceThrow.SpellAttack(character = character, spell = id)
return DiceThrow.SpellAttack(character = characterName, spell = id)
}
fun onSpellDamageRoll(id: String): DiceThrow {
return DiceThrow.SpellDamage(character = character, spell = id)
return DiceThrow.SpellDamage(character = characterName, spell = id)
}
/**
@ -216,4 +234,11 @@ class SpellsActionViewModel @Inject constructor(
else -> 0
}
}
private class Struct(
val sheets: Map<String, CharacterSheet>,
val spells: Map<String, List<AssignedSpell>>,
) {
lateinit var fire: CharacterSheetFire
}
}

View file

@ -0,0 +1,129 @@
package com.pixelized.rplexicon.ui.screens.character.pages.actions
import android.app.Application
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.CharacterSheetFire
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.composable.edit.CounterEditDialogUio
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItemUio
import com.pixelized.rplexicon.utilitary.extentions.context
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class TokenViewModel @Inject constructor(
private val characterRepository: CharacterSheetRepository,
private val firebaseRepository: FirebaseRepository,
private val alterationRepository: AlterationRepository,
application: Application,
savedStateHandle: SavedStateHandle,
) : AndroidViewModel(application) {
private val character = savedStateHandle.characterSheetArgument.name
private val _dialog = mutableStateOf<CounterEditDialogUio?>(null)
val dialog: State<CounterEditDialogUio?> get() = _dialog
private val _counters = mutableStateOf<List<TokenItemUio>>(emptyList())
val tokens: State<List<TokenItemUio>> get() = _counters
init {
viewModelScope.launch {
launch(Dispatchers.IO) {
characterRepository.data
.combine(firebaseRepository.getCharacter(character = character)) { sheets, fire ->
sheets.getValue(character) to fire
}
.collect { data ->
val (character, fire) = data
val counters = mutableListOf<TokenItemUio>()
character.rage?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.RAGE,
icon = R.drawable.ic_fist_24,
label = R.string.token_label_rage,
value = fire.rage ?: 0,
max = it,
)
)
}
character.relentlessEndurance?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.RELENTLESS_ENDURANCE,
icon = R.drawable.ic_burning_passion_24,
label = R.string.token_label_relentless_endurance,
value = fire.relentlessEndurance ?: 0,
max = it,
)
)
}
character.bardicInspiration?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.BARDIC_INSPIRATION,
icon = R.drawable.ic_lyre_24,
label = R.string.token_label_bardic_inspiration,
value = fire.bardicInspiration ?: 0,
max = it,
)
)
}
character.divineConduit?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.DIVINE_CONDUIT,
icon = R.drawable.ic_embrassed_energy_24,
label = R.string.token_label_divine_conduit,
value = fire.divineConduit ?: 0,
max = it,
)
)
}
withContext(Dispatchers.Main) {
_counters.value = counters
}
}
}
}
}
fun showTokenEditDialog(dialog: TokenItemUio) {
_dialog.value = CounterEditDialogUio(
id = dialog.id,
label = context.getString(dialog.label),
value = dialog.value,
max = dialog.max,
)
}
fun showSpellTokenEditDialog(level: Int, value: Int, max: Int) {
_dialog.value = CounterEditDialogUio(
id = CharacterSheetFire.SPELL_SLOT_LEVEL_X + level,
label = context.getString(R.string.spell_level_chooser_label, "$level"),
value = value,
max = max,
)
}
fun hideCounterEditDialog() {
_dialog.value = null
}
fun applyCounterValue(id: String, value: Int) {
firebaseRepository.setToken(character = character, token = id, value = value)
_dialog.value = null
}
}

View file

@ -34,12 +34,12 @@ fun AlterationPage(
AlterationPageContent(
alterations = viewModel.alterations,
onInfo = {
onAlterationInfo = {
viewModel.showAlterationDetail(id = it)
},
onClick = {
onAlterationClick = {
scope.launch {
viewModel.toggle(alteration = it)
viewModel.toggleAlteration(alteration = it)
}
},
)
@ -54,8 +54,8 @@ fun AlterationPage(
fun AlterationPageContent(
modifier: Modifier = Modifier,
alterations: State<List<RollAlterationUio>>,
onInfo: (String) -> Unit,
onClick: (String) -> Unit,
onAlterationInfo: (String) -> Unit,
onAlterationClick: (String) -> Unit,
) {
LazyColumn(
modifier = modifier,
@ -65,8 +65,8 @@ fun AlterationPageContent(
RollAlteration(
modifier = Modifier.fillMaxWidth(),
alteration = it,
onInfo = onInfo,
onClick = onClick,
onInfo = onAlterationInfo,
onClick = onAlterationClick,
)
}
}
@ -98,8 +98,8 @@ fun AlterationPagePreview() {
Surface {
AlterationPageContent(
alterations = rememberRollAlterations(),
onInfo = { },
onClick = { },
onAlterationInfo = { },
onAlterationClick = { },
)
}
}

View file

@ -5,7 +5,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.rolls.composable.AlterationDetailUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
@ -18,7 +20,7 @@ import javax.inject.Inject
@HiltViewModel
class AlterationViewModel @Inject constructor(
private val repository: AlterationRepository,
private val alterationRepository: AlterationRepository,
private val factory: AlterationFactory,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
@ -32,24 +34,28 @@ class AlterationViewModel @Inject constructor(
init {
viewModelScope.launch {
repository.assignedAlterations.collect {
_alterations.value = withContext(Dispatchers.IO) {
launch(Dispatchers.IO) {
alterationRepository.assignedAlterations.collect {
val alterations = it[character] ?: emptyList()
factory.convert(character = character, alterations = alterations)
.sortedBy { it.label }
.sortedBy { it.subLabel }
val data = factory.convert(character = character, alterations = alterations)
.sortedBy { alteration -> alteration.label }
.sortedBy { alteration -> alteration.subLabel }
withContext(Dispatchers.Main) {
_alterations.value = data
}
}
}
}
}
suspend fun toggle(alteration: String) {
val value = repository.getStatus(character = character, alteration = alteration)
repository.setStatus(character = character, alteration = alteration, value.not())
suspend fun toggleAlteration(alteration: String) {
val value = alterationRepository.getStatus(character = character, alteration = alteration)
alterationRepository.setStatus(character = character, alteration = alteration, value.not())
}
fun showAlterationDetail(id: String) {
val alteration = repository.getAlterations(character = character).firstOrNull { it.name == id }
val alteration = alterationRepository.getAlterations(character = character)
.firstOrNull { it.name == id }
if (alteration != null) {
_alterationDetail.value = AlterationDetailUio(
name = id,

View file

@ -0,0 +1,156 @@
package com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class TokenItemUio(
val id: String,
@DrawableRes val icon: Int,
@StringRes val label: Int,
val value: Int,
val max: Int,
)
@Composable
fun TokenItem(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
counter: TokenItemUio,
onClick: (TokenItemUio) -> Unit,
) {
Box(
modifier = Modifier
.clickable { onClick(counter) }
.heightIn(min = 52.dp)
.padding(paddingValues = padding)
.then(other = modifier),
contentAlignment = Alignment.Center,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
Icon(
painter = painterResource(id = counter.icon),
contentDescription = null,
)
Text(
modifier = Modifier
.alignByBaseline()
.weight(weight = 1f),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(id = counter.label),
)
Row(
modifier = Modifier.alignByBaseline(),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
text = "${counter.value}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Light,
text = "/"
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Light,
text = "${counter.max}",
)
}
}
}
}
@Composable
@Stable
fun rememberTokenListStatePreview(): State<List<TokenItemUio>> = remember {
mutableStateOf(
listOf(
TokenItemUio(
id = "1",
icon = R.drawable.ic_fist_24,
label = R.string.token_label_rage,
value = 2,
max = 2,
),
TokenItemUio(
id = "2",
icon = R.drawable.ic_embrassed_energy_24,
label = R.string.token_label_divine_conduit,
value = 2,
max = 4,
),
TokenItemUio(
id = "3",
icon = R.drawable.ic_lyre_24,
label = R.string.token_label_bardic_inspiration,
value = 2,
max = 3,
),
)
)
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun CounterItemPreview() {
LexiconTheme {
Surface {
TokenItem(
counter = TokenItemUio(
id = "0",
icon = R.drawable.ic_burning_passion_24,
label = R.string.token_label_relentless_endurance,
value = 2,
max = 2,
),
onClick = { },
)
}
}
}

View file

@ -13,9 +13,9 @@ import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.character.ProficiencyUio
import com.pixelized.rplexicon.ui.screens.character.composable.character.StatUio
import com.pixelized.rplexicon.ui.screens.character.factory.CharacterSheetUioFactory
import com.pixelized.rplexicon.utilitary.extentions.local.toActiveStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -36,15 +36,18 @@ class ProficiencyViewModel @Inject constructor(
init {
viewModelScope.launch {
launch {
characterRepository.data.collect {
_sheet.value = withContext(Dispatchers.Default) {
val alterations = alterationRepository.getAlterations(character)
characterSheetFactory.convert(
sheet = it.getValue(key = character),
alterations = alterations.toActiveStatus()
)
characterRepository.data
.combine(alterationRepository.assignedAlterations) { sheets, _ -> sheets }
.collect {
_sheet.value = withContext(Dispatchers.Default) {
val alterations =
alterationRepository.getActiveAlterationsStatus(character)
characterSheetFactory.convert(
sheet = it.getValue(key = character),
alterations = alterations,
)
}
}
}
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@ -30,6 +31,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
@ -48,9 +50,12 @@ fun AlterationDetail(
) {
Surface(
modifier = Modifier
.padding(all = 24.dp)
.padding(all = 16.dp)
.ddBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)
.then(other = modifier),
shape = remember { RoundedCornerShape(size = 24.dp) },
) {
Column {
Row(

View file

@ -5,6 +5,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@ -38,6 +39,7 @@ data class RollAlterationUio(
@Composable
fun RollAlteration(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
alteration: RollAlterationUio,
onInfo: (id: String) -> Unit,
onClick: (id: String) -> Unit,
@ -46,7 +48,7 @@ fun RollAlteration(
modifier = Modifier
.clickable { onClick(alteration.label) }
.heightIn(min = 52.dp)
.padding(horizontal = 16.dp)
.padding(paddingValues = padding)
.then(other = modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,

View file

@ -66,8 +66,8 @@ fun lightColorScheme(
),
placeholder: Color = Color(red = 230, green = 225, blue = 229),
sheet: LexiconColors.CharacterSheet = LexiconColors.CharacterSheet(
innerBorder = base.onSurface.copy(alpha = 0.35f),
outlineBorder = base.onSurface.copy(alpha = 0.6f),
innerBorder = base.onSurface.copy(alpha = 0.1f),
outlineBorder = base.onSurface.copy(alpha = 0.3f),
),
) = colorScheme(
base = base,

View file

@ -28,7 +28,7 @@ class LexiconTypography(
val stamp: TextStyle = base.headlineLarge.copy(
fontFamily = stampFontFamily,
),
val bodyDropCap: TextStyle = base.displaySmall.copy(
val bodyDropCap: TextStyle = base.headlineLarge.copy(
fontFamily = zallFontFamily,
baselineShift = BaselineShift(-0.1f),
letterSpacing = (-3).sp

View file

@ -92,6 +92,10 @@ fun Modifier.ddBorder(
color = colorScheme.characterSheet.outlineBorder,
shape = outline,
)
.background(
shape = outline,
color = colorScheme.base.surfaceColorAtElevation(elevation.value)
)
.padding(
horizontal = horizontalSpacing,
vertical = verticalSpacing,

View file

@ -13,18 +13,6 @@ fun List<Alteration>.toStatus(): Map<Property, List<Alteration.Status>> {
return status
}
fun List<Alteration>.toActiveStatus(): Map<Property, List<Alteration.Status>> {
val status = hashMapOf<Property, MutableList<Alteration.Status>>()
forEach { alteration ->
if (alteration.active) {
alteration.status.forEach {
status.getOrPut(it.key) { mutableListOf() }.add(it.value)
}
}
}
return status
}
val List<Alteration.Status>?.sum: Int
get() = this?.sumOf { alt -> alt.bonus.sumOf { it.value } } ?: 0

View file

@ -1,9 +1,22 @@
package com.pixelized.rplexicon.utilitary.extentions.local
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.Counter
import com.pixelized.rplexicon.model.CharacterSheetFire
fun CharacterSheet.spell(level: Int): Counter? = when (level) {
fun CharacterSheet.spell(level: Int): Int? = when (level) {
1 -> spell1
2 -> spell2
3 -> spell3
4 -> spell4
5 -> spell5
6 -> spell6
7 -> spell7
8 -> spell8
9 -> spell9
else -> null
}
fun CharacterSheetFire.spell(level: Int): Int? = when (level) {
1 -> spell1
2 -> spell2
3 -> spell3

View file

@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M244.19,21.97C347.89,119.18 428.1,216.27 494.72,304.06L494.72,191.66c-7.06,-6.39 -14.15,-12.68 -21.34,-18.84 2.99,-3.46 4.81,-7.97 4.81,-12.91 0,-10.93 -8.85,-19.78 -19.78,-19.78 -6.28,0 -11.88,2.96 -15.5,7.53 -43.25,-34.45 -88.29,-64.96 -134,-91.31 1.01,-2.03 1.59,-4.27 1.59,-6.69 0,-8.35 -6.77,-15.13 -15.13,-15.13 -5.42,0 -10.14,2.85 -12.81,7.13 -12.76,-6.88 -25.57,-13.45 -38.38,-19.69zM104.63,40.09c5.11,5.18 10.18,10.36 15.22,15.59 -4.22,1.17 -7.38,4.29 -8.35,9 -1.81,8.84 4.73,19.92 14.63,24.72 3.64,1.77 7.24,2.42 10.41,2.12 -2.54,4.11 -4,8.97 -4,14.16 0,14.89 12.05,26.94 26.94,26.94 8.68,0 16.41,-4.1 21.34,-10.47 88.69,101.2 165.6,208.4 235.16,306.72 -7.4,6.05 -12.13,15.23 -12.13,25.53 0,18.21 14.76,32.97 32.97,32.97 6.27,0 12.13,-1.77 17.12,-4.81 3.09,4.35 6.19,8.76 9.25,13.06h31.53L494.72,381.28c-41.36,-69.22 -154.77,-193.34 -184.53,-213.31 24.14,33.4 45.1,64.34 64.81,94.03 -87.49,-95.75 -183.44,-179.59 -270.38,-221.91zM76.4,55.34c-1.87,0.09 -3.74,0.55 -5.53,1.41 -7.18,3.43 -10.21,12.04 -6.78,19.22 3.43,7.18 12.04,10.21 19.22,6.78 7.18,-3.43 10.21,-12.04 6.78,-19.22 -2.57,-5.38 -8.08,-8.44 -13.69,-8.19zM432.15,75.72c-1.45,0.05 -2.86,0.36 -4.16,0.97 -6.92,3.26 -8.27,13.63 -3,23.16 5.26,9.53 15.14,14.63 22.06,11.38 6.92,-3.26 8.26,-13.63 3,-23.16 -4.28,-7.74 -11.6,-12.56 -17.91,-12.34zM281.03,110.97c-1.98,0.12 -3.85,0.72 -5.44,1.78 -6.37,4.24 -6.17,14.69 0.44,23.34 6.61,8.65 17.1,12.24 23.47,8 6.37,-4.24 6.17,-14.69 -0.44,-23.34 -4.95,-6.49 -12.08,-10.15 -18.03,-9.78zM25.19,149.28c91.02,100.04 158.7,190.4 212,271.6 -8.48,-1.36 -18.44,4.95 -20.25,13.59 -6.22,29.78 12.9,53.91 47.59,61.15L390.19,495.62c-97.97,-139.74 -234.78,-282.95 -365,-346.34zM55.22,251.13c-3.81,0.08 -7.2,1.33 -9.41,3.84 -5.05,5.74 -2.15,15.78 6.47,22.44 8.62,6.65 19.7,7.4 24.75,1.66 5.05,-5.74 2.18,-15.79 -6.44,-22.44 -4.85,-3.74 -10.48,-5.6 -15.38,-5.5zM102.41,289.25c-14.89,0 -26.94,12.05 -26.94,26.94 0,14.89 12.05,26.97 26.94,26.97 14.89,0 26.97,-12.08 26.97,-26.97 0,-14.89 -12.08,-26.94 -26.97,-26.94zM182.13,392.72c-10.93,0 -19.78,8.85 -19.78,19.78 0,10.93 8.85,19.78 19.78,19.78 10.93,0 19.78,-8.85 19.78,-19.78 0,-10.93 -8.85,-19.78 -19.78,-19.78z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M19.84,18.75v2.72c64.27,50.96 95.31,115.85 89.44,179.25 -10.6,-55 -41.76,-104.8 -89.44,-138.91v23.53c55.24,45.18 82.41,114.31 72.97,185.25 -0.4,2.33 -0.76,4.66 -1.06,7 -0.09,0.51 -0.16,1.02 -0.25,1.53h0.06c-6.55,53.8 11.2,108.57 49.59,156.03 6.41,11.07 13.98,21.8 22.69,32.13C95.25,406.66 59.08,335.53 53.22,262.41c-11.11,83 15.11,163.21 90.69,230.22L188.5,492.63c0.03,0.03 0.06,0.06 0.09,0.09h130.47c0.03,-0.03 0.06,-0.06 0.09,-0.09h43.63c75.58,-67.01 101.8,-147.22 90.69,-230.22 -5.75,71.77 -40.7,141.62 -106.85,201.5 9.56,-11.75 17.68,-24.02 24.28,-36.69 34.1,-45.58 49.6,-97.28 43.41,-148.1 -0,-0.02 0,-0.04 0,-0.06 -0.41,-3.31 -0.91,-6.61 -1.5,-9.91 -9.36,-74.25 21.31,-146.35 82.31,-190.88L495.13,55.63c-52.75,34.07 -87.21,86.5 -98.53,144.84 -5.85,-64.21 26.1,-129.92 92.12,-181.13l-0.47,-0.59h-28.06c-72.05,64.34 -99.85,149.67 -72.5,228.06 2.89,8.29 5.11,16.68 6.66,25.09 0,0.04 0.03,0.08 0.03,0.13 0.44,3.43 0.93,6.88 1.53,10.31h0.03c2.3,19.37 1.12,38.89 -3.4,58.16 -0.04,-28.38 -6.78,-57.15 -20.44,-85.06 -40.06,-81.86 -20.77,-171.43 52.41,-236.69h-31.03c-50.15,46.62 -66.32,91.56 -57.44,151.09 -21.49,-59.17 -19.42,-103.58 20.69,-151.09L152,18.75c40.1,47.51 42.18,91.93 20.69,151.09 8.89,-59.53 -7.27,-104.47 -57.41,-151.09L83.16,18.75c73.17,65.26 92.46,154.82 52.41,236.69 -14.9,30.45 -21.52,61.92 -20.25,92.78 -6.15,-21.75 -8.02,-43.91 -5.41,-65.87 0.6,-3.44 1.12,-6.88 1.56,-10.31 1.55,-8.46 3.78,-16.88 6.69,-25.22 27.35,-78.39 -0.45,-163.72 -72.5,-228.06L19.84,18.75zM254.09,38.44c16.4,0 27.02,6.18 34.72,16.59 7.69,10.41 11.97,25.73 11.97,43 0,18.66 -6.89,38.56 -15.97,49.5l-10.13,12.22 15.59,2.94c12.52,2.35 21.72,8.77 29.44,19 7.72,10.23 13.57,24.36 17.69,40.69 7.52,29.84 9.14,66.52 9.38,99.34h-23.31l-0.81,-70.5 -18.69,0.22 0.97,86.44 -7.75,111.63c47.06,-43.67 71.99,-94.3 76.16,-146.31 8.21,61.34 -11.15,120.61 -67,170.13L295.53,473.31v0.22h-32.94L262.59,333.81h-18.69v139.72L212.62,473.53v-0.22h-9.65c-55.85,-49.52 -75.24,-108.79 -67.03,-170.13 4.13,51.56 28.66,101.78 74.94,145.19L203.78,345.72l2.6,-94.13 -18.69,-0.53 -1.94,70.65h-24.38c0.24,-32.83 1.88,-69.5 9.41,-99.35 4.12,-16.33 9.97,-30.46 17.69,-40.69 7.72,-10.23 16.92,-16.65 29.44,-19l15.59,-2.94 -10.13,-12.22c-9.08,-10.94 -15.97,-30.83 -15.97,-49.5 0,-17.27 4.28,-32.59 11.97,-43 7.69,-10.41 18.32,-16.59 34.72,-16.59z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M179.81,20.72v81.25L135.78,75.62l17.56,46.94 -115.66,-20.94 84.72,49.91H20v27.34l110.47,14.88 96.59,-29.19c-11.3,-11.87 -18.59,-30.74 -18.59,-52 0,-35.93 20.87,-65.06 46.62,-65.06 25.75,0 46.63,29.14 46.63,65.06 0,20.85 -7.04,39.38 -17.97,51.28l99.03,29.91 112.5,-15.16V151.53H394.19l84.72,-49.9 -120.44,21.78 17.87,-47.72 -48.66,29.13V20.72H179.81zM495.28,223.34l-112.5,22.44 -55.4,-13.12 -28.03,118.31 16.59,145h51.69L329.25,351.22l46.53,27.84 -21.31,-56.94 124.44,22.5 -91.13,-53.69h107.5v-67.59zM20,223.75v67.19h108.81l-91.13,53.69L157.31,322.97 136.35,379l38.47,-23 -28.59,139.97h48.15L207.28,351.56 185.6,232.72l-55.13,13.06L20,223.75z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M227.23,21.78c-1.85,0 -3.7,0.05 -5.57,0.16 -15.31,0.88 -30.76,5.3 -39.49,10.86l-0.01,73.15c2.88,-0.09 5.78,-0.15 8.68,-0.14 23.38,0.04 47.1,3.29 68.47,9.51l0.01,-87.51c-7.03,-3.52 -19.18,-6.03 -32.09,-6.03zM307.97,30.94c-11.93,0.15 -23.08,2.36 -29.97,5.6l-0.01,77.6v7.66c38.49,15.67 64.81,42.48 58.74,78.76l-0.96,5.73 -5.56,1.67c-17.45,5.25 -34.87,9.7 -52.22,13.34L277.98,246.53c25.56,-0.7 51.33,-2.69 77.14,-6.1l0.02,-197.93c-8.28,-5.56 -23.51,-10.24 -38.84,-11.33 -2.79,-0.2 -5.58,-0.27 -8.34,-0.24zM143.22,46.29c-1.18,-0.01 -2.37,-0.01 -3.59,0.02 -4.18,0.1 -8.53,0.47 -12.9,1.15 -15.67,2.45 -31.48,8.56 -40.41,15.4l-0.01,72.96c18.81,-15.81 46.7,-25.14 77.15,-28.54l0.01,-57.97c-4.82,-1.75 -12.02,-2.92 -20.25,-3.02zM401.62,49.75c-10.8,0.12 -20.72,1.93 -27.04,4.66l-0.02,183.18c25.07,-4.02 50.16,-9.41 75.12,-16.36l1.99,-158.45c-8.35,-5.9 -23.65,-11.02 -39.05,-12.55 -3.7,-0.37 -7.4,-0.52 -11,-0.48zM178.84,123.96c-53.72,0.7 -101.41,20.36 -97.89,66.6 15.84,-3.92 30.84,-5.89 44.94,-6.1 34.84,-0.51 64.21,9.7 87.32,27.61 34.61,-3.11 69.85,-10 105.41,-20.31 0.14,-41.29 -74.1,-68.66 -139.78,-67.8zM129.97,202.61c-1.3,-0 -2.6,0.01 -3.92,0.05 -17.26,0.44 -36.45,4.03 -57.57,11.04 5.79,53.81 26.33,106.41 58.5,143.35 6.23,7.15 12.86,13.71 19.88,19.61 29.3,9.28 69.26,12.92 110.53,12.14 3.78,-55.81 -8.72,-108.36 -36.19,-142.74 -21.26,-26.61 -51.06,-43.39 -91.23,-43.44zM259.29,224.89c-9.36,1.64 -18.69,3.02 -28,4.15 1.54,1.74 3.04,3.52 4.5,5.35 3.15,3.94 6.09,8.06 8.87,12.33 9.92,0.14 19.87,0.13 29.86,-0.11L259.29,246.61v-21.72zM451.11,240.23c-65.41,17.83 -131.46,25.41 -195.85,25.32 17,35.14 23.83,78.09 21.01,122.6 42.48,-2.08 85.03,-8.23 118.19,-15.98 26.69,-32.78 47.37,-77.12 56.65,-131.93zM400.51,389.9c-38.33,9.15 -87.95,16.06 -136.87,17.45 -47.67,1.36 -94.34,-2.23 -129.45,-15.26l-0.01,78.93c27.19,12.57 76.41,20.2 127.32,20.3 51.22,0.09 104.21,-7.17 139,-20.77l0.01,-80.65z" />
</vector>

View file

@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M129.66,21.19L37.94,79.78c3.54,26.81 8.91,53.55 16.13,80.13L240.72,39.59l-19.28,-12.5c-31.28,-0.88 -62.2,-2.84 -91.78,-5.91zM383.13,21.81c-40.51,3.97 -83.5,5.94 -126.47,5.84l204.63,132.72c7.11,-25.89 12.49,-51.92 16.09,-78.03l-94.25,-60.53zM257.94,50.75L59.47,178.66c8.02,26.32 17.86,52.46 29.53,78.31l243.25,-158 -74.31,-48.22zM349.41,110.09l-74.56,48.44 151.28,98.78c11.71,-25.8 21.59,-51.91 29.69,-78.19l-106.41,-69.03zM257.72,169.66L97,274.06c12.2,25.17 26.14,50.06 41.84,74.56l196.09,-128.53 -77.22,-50.44zM352,231.22l-77.53,50.84 101.4,67.19c15.82,-24.6 29.9,-49.58 42.22,-74.88L352,231.22zM257.47,293.19l-108.35,71.03c13.56,20.06 28.33,39.85 44.28,59.31l132.03,-85.28 -67.97,-45.06zM342.44,349.5L274.5,393.41l47.03,30.38c15.85,-19.34 30.51,-38.99 44.03,-58.94L342.44,349.5zM257.47,404.38L205.5,437.97c16.23,18.93 33.61,37.54 52.16,55.78 18.39,-18.15 35.64,-36.68 51.78,-55.53l-52.09,-33.63 0.13,-0.22z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M108.66,35.06c-15.05,0.14 -33.41,5.38 -46.97,15.81 -10.75,8.28 -18.78,19.27 -21.19,34.44 0.21,-0.13 0.41,-0.25 0.63,-0.38 -0.84,2.82 -1.31,5.79 -1.31,8.88 0,17.09 13.85,30.94 30.94,30.94 14.29,0 26.32,-9.7 29.88,-22.88 0.03,-0.12 0.07,-0.23 0.09,-0.34 0.48,-2.08 0.77,-4.04 0.9,-5.84 0.03,-0.33 0.02,-0.65 0.03,-0.97 0,-0.13 0.03,-0.25 0.03,-0.38 0.01,-0.18 -0,-0.35 0,-0.53 0,-1.53 -0.1,-3.04 -0.31,-4.5 -1.37,-8.02 -6.78,-12.16 -12.59,-13.72 -8.53,-2.29 -19.06,0.64 -23.75,18.16l-0.47,-0.13C62.04,74.12 72.21,63.88 83.94,60.78c2.48,-0.65 5.05,-1 7.66,-0.97 9.07,0.13 18.44,4.88 24.56,17.13 0.09,0.17 0.17,0.35 0.25,0.53 5.21,15.23 2.11,43.32 -3.34,57.63 -7.29,18.75 -22.38,40.5 -47.69,65.5C6.99,258.25 4,329.82 39.97,388.81 75.94,447.8 152.13,493.56 254.44,493.56c102.31,0 178.47,-45.76 214.44,-104.75 35.88,-58.85 32.98,-130.23 -25,-187.81l-0.41,-0.41h-0.03c-25.31,-25 -40.37,-46.75 -47.66,-65.5 -5.23,-16.45 -9.09,-42.99 -2.65,-57.63 0.06,-0.13 0.13,-0.25 0.19,-0.38 0.03,-0.05 0.04,-0.11 0.06,-0.16 6.12,-12.25 15.49,-17 24.56,-17.13 2.6,-0.04 5.18,0.31 7.66,0.97 11.72,3.1 21.87,13.34 19.34,32.84l-0.44,0.13c-4.69,-17.52 -15.22,-20.45 -23.75,-18.16 -4.41,1.18 -8.6,3.85 -10.97,8.56 -0.01,0.04 -0.02,0.09 -0.03,0.13 -0.98,3.01 -1.5,6.2 -1.5,9.53 0,17.09 13.85,30.94 30.94,30.94 17.09,0 30.94,-13.85 30.94,-30.94 0,-4.36 -0.91,-8.49 -2.53,-12.25 -3.06,-13.24 -10.6,-23.11 -20.44,-30.69 -14.46,-11.13 -34.39,-16.36 -49.94,-15.78 -13.38,0.5 -24.85,4.11 -33.22,10.53 -3.41,2.62 -6.38,5.7 -8.84,9.38 -69.46,35.51 -138.89,38.75 -208.34,-7.75 -0.64,-0.56 -1.29,-1.11 -1.97,-1.63 -8.37,-6.42 -19.84,-10.03 -33.22,-10.53 -0.97,-0.04 -1.96,-0.04 -2.97,-0.03zM161.44,88.19c6.34,2.65 12.67,4.99 19,7.03v313.06c-30.73,-8.26 -57.89,-22 -77.37,-41.31 -17.1,-16.94 -28.08,-38.63 -28.91,-63.6 -0.83,-24.97 8.27,-52.7 28.97,-82.63 41.32,-59.75 57.16,-103.6 58.31,-132.56zM347.47,89.59c1.6,28.97 17.59,72.37 58.25,131.16 20.69,29.92 29.8,57.66 28.97,82.63 -0.83,24.97 -11.81,46.65 -28.91,63.59 -19.02,18.85 -45.37,32.4 -75.22,40.72L330.56,94.84c5.64,-1.61 11.27,-3.35 16.91,-5.25zM311.87,99.59v312.53c-8.21,1.63 -16.61,2.88 -25.13,3.78L286.75,104.09c8.38,-1.12 16.75,-2.63 25.13,-4.5zM199.12,100.37c8.45,1.97 16.89,3.44 25.34,4.44v311.34c-8.59,-0.84 -17.06,-2.05 -25.34,-3.63L199.12,100.38zM268.06,105.94v311.38c-4.54,0.2 -9.09,0.31 -13.66,0.31 -3.76,0 -7.51,-0.05 -11.25,-0.19L243.16,106.28c8.29,0.31 16.61,0.18 24.91,-0.34z" />
</vector>

View file

@ -154,4 +154,10 @@
<string name="alteration_source">Source : %1$s</string>
<string name="alteration_target">Cible : %1$s</string>
<string name="token_label_title">Capacité</string>
<string name="token_label_rage">Rage</string>
<string name="token_label_relentless_endurance">Endurance Implacable</string>
<string name="token_label_bardic_inspiration">Inspiration Bardique</string>
<string name="token_label_divine_conduit">Conduit Divin</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="google_spreadsheet_api_key">AIzaSyBmagVOEyB68tTJ5QMFMzMQZIHG_4XVCOo</string>
<string name="google_sign_in_id" translatable="false">62913404482-ergqkjiuvint49q8lm555j21vvb6af7s.apps.googleusercontent.com</string>
<string name="google_spreadsheet_api_key" translatable="false">AIzaSyBmagVOEyB68tTJ5QMFMzMQZIHG_4XVCOo</string>
<string name="firebase_realtime_database" translatable="false">https://rp-lexicon-default-rtdb.europe-west1.firebasedatabase.app/</string>
</resources>

View file

@ -75,7 +75,7 @@
<string name="map_label">Coordinates</string>
<string name="character_sheet_title">Character sheet</string>
<string name="character_sheet_title_hp">HP</string>
<string name="character_sheet_title_hp">Hit Point</string>
<string name="character_sheet_title_ca">CA</string>
<string name="character_sheet_title_dc">DC</string>
<string name="character_sheet_title_speed">Speed</string>
@ -154,4 +154,10 @@
<string name="alteration_source">Source: %1$s</string>
<string name="alteration_target">Target: %1$s</string>
<string name="token_label_title">Skill</string>
<string name="token_label_rage">Rage</string>
<string name="token_label_relentless_endurance">Relentless Endurance</string>
<string name="token_label_bardic_inspiration">Bardic Inspiration</string>
<string name="token_label_divine_conduit">Divine Conduit</string>
</resources>