diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1253268..29748bf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -133,6 +133,8 @@ dependencies { implementation(platform("com.google.firebase:firebase-bom:32.3.1")) implementation("com.google.firebase:firebase-crashlytics-ktx") implementation("com.google.firebase:firebase-analytics-ktx") + implementation("com.google.firebase:firebase-auth-ktx") + implementation("com.google.firebase:firebase-database-ktx") // Hilt: Dependency injection implementation("androidx.hilt:hilt-navigation-compose:1.0.0") diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 15c5ecf..2e77bb4 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -21,9 +21,18 @@ -renamesourcefileattribute SourceFile ## --------------- GOOGLE API --------------- - -keepclassmembers class * { @com.google.api.client.util.Key ; } -keep class com.google.api.services.sheets.** { *; } --keep class com.fasterxml.** { *; } \ No newline at end of file +-keep class com.fasterxml.** { *; } + +## --------------- Firebase RealTime Database --------------- + + # Add this global rule +-keepattributes Signature + +# This rule will properly ProGuard all the model classes in +# the package com.yourcompany.models. +# Modify this rule to fit the structure of your app. +-keepclassmembers class com.pixelized.rplexicon.model.CharacterSheetFire.** { *; } \ No newline at end of file diff --git a/app/src/debug/res/drawable/ic_half_heart_24.xml b/app/src/debug/res/drawable/ic_half_heart_24.xml new file mode 100644 index 0000000..ff368a1 --- /dev/null +++ b/app/src/debug/res/drawable/ic_half_heart_24.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt index 0fbbf39..1a25ff5 100644 --- a/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt +++ b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt @@ -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 diff --git a/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheetFire.kt b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheetFire.kt new file mode 100644 index 0000000..4e31826 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheetFire.kt @@ -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_" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/authentication/AuthenticationRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/authentication/AuthenticationRepository.kt index c67f4a0..9aa94ec 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/authentication/AuthenticationRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/authentication/AuthenticationRepository.kt @@ -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(null) - val isAuthenticated: State = 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(null) +// val isAuthenticated: State = 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, +// ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/authentication/FirebaseRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/authentication/FirebaseRepository.kt new file mode 100644 index 0000000..ccb7369 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/repository/authentication/FirebaseRepository.kt @@ -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() + val error: SharedFlow get() = _error + + init { + Firebase.database.setPersistenceEnabled(true) + } + + fun getCharacter(character: String): Flow { + 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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/data/AlterationRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/data/AlterationRepository.kt index 17f7b75..025d5bb 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/data/AlterationRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/data/AlterationRepository.kt @@ -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 { 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 { 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> { + val status = hashMapOf>() + _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 } diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/data/CharacterSheetRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/data/CharacterSheetRepository.kt index 6935705..1f97433 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/data/CharacterSheetRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/data/CharacterSheetRepository.kt @@ -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 diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt b/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt index abc318c..b6cd3f0 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt @@ -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 { @@ -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, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/IndicatorStep.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/IndicatorStep.kt index 00e234f..27055a3 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/IndicatorStep.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/IndicatorStep.kt @@ -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( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/NumberPicker.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/NumberPicker.kt new file mode 100644 index 0000000..3d36a8f --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/NumberPicker.kt @@ -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 } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleCounterEditDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleCounterEditDialog.kt new file mode 100644 index 0000000..27ce036 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleCounterEditDialog.kt @@ -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, + 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)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleHitPointEditDialog.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleHitPointEditDialog.kt new file mode 100644 index 0000000..7252208 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/edit/HandleHitPointEditDialog.kt @@ -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, + 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 = { _, _ -> }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt index 57d4add..f51eb2c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt @@ -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 { @Composable fun ScreenNavHost( navHostController: NavHostController = rememberNavController(), - startDestination: String = HOME_ROUTE, + startDestination: String = AUTHENTICATION_ROUTE, ) { val lexiconListState = rememberLazyListState() val questListState = rememberLazyListState() diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt index 44a708e..4046801 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt @@ -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, + 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) }, + ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt index c07cdfc..f91dd9f 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt @@ -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? = null - private val state = mutableStateOf( - when (repository.isAuthenticated.value) { - true -> AuthenticationStateUio.Success - else -> AuthenticationStateUio.Initial - } + private val state = mutableStateOf( + 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 { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt index d7e86ae..7e5d46f 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt @@ -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() }, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt index 8652a7b..4cf1749 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt @@ -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(null) - val header: State get() = _header - private val _isLoading = mutableStateOf(false) val isLoading: State 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) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt index 6eb6727..7973f22 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/AttackItem.kt @@ -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 = { }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellHeader.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellHeader.kt index 7aadd1a..d6b866b 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellHeader.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellHeader.kt @@ -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 = { _, _, _ -> }, ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellItem.kt index 8e47842..27a953d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/SpellItem.kt @@ -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, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/CharacterSheetHeader.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/CharacterSheetHeader.kt index 4bc5fd0..f899b78 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/CharacterSheetHeader.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/CharacterSheetHeader.kt @@ -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, + 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 = { }, ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/LabelPoint.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/LabelPoint.kt index 2a06ea5..c55d749 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/LabelPoint.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/character/LabelPoint.kt @@ -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( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/CharacterSheetHeaderUioFactory.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/CharacterSheetHeaderUioFactory.kt index f14d173..7600d7c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/CharacterSheetHeaderUioFactory.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/CharacterSheetHeaderUioFactory.kt @@ -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>, + 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}" + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt index c93d79d..d77731a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt @@ -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, attacks: State>, + tokens: State>, spells: State>>>, + 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 = { }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt index af85d70..571bace 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt @@ -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() + } } - } } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/HeaderViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/HeaderViewModel.kt new file mode 100644 index 0000000..5a05d98 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/HeaderViewModel.kt @@ -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(null) + private val fireData = mutableStateOf(null) + + val header: State = derivedStateOf { + headerFactory.convert( + sheetHeaderData = sheetData.value, + fireHeaderData = fireData.value, + ) + } + + private val _hitPointDialog = mutableStateOf(null) + val hitPointDialog: State 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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt index a73a1e9..8ce9614 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt @@ -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>>>(emptyList()) val spells: State>>> 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, + val spells: Map>, + ) { + lateinit var fire: CharacterSheetFire + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/TokenViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/TokenViewModel.kt new file mode 100644 index 0000000..2b02e44 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/TokenViewModel.kt @@ -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(null) + val dialog: State get() = _dialog + + private val _counters = mutableStateOf>(emptyList()) + val tokens: State> 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() + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationPage.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationPage.kt index 0566d13..c75a68e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationPage.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationPage.kt @@ -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>, - 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 = { }, ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationViewModel.kt index 23f5282..3c1bfba 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/AlterationViewModel.kt @@ -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, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/composable/TokenItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/composable/TokenItem.kt new file mode 100644 index 0000000..b7afd3f --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/alteration/composable/TokenItem.kt @@ -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> = 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 = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt index 12847f4..6ea45cb 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt @@ -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, + ) + } } - } } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/AlterationDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/AlterationDetail.kt index 8397e0d..1632d0b 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/AlterationDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/AlterationDetail.kt @@ -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( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollAlteration.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollAlteration.kt index c24f3d2..d8aa6ad 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollAlteration.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollAlteration.kt @@ -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, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt index 78ad3af..1f89399 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt @@ -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, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt index 19d6a6a..37e8ee5 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt @@ -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 diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt index 7421d5b..96849dd 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt @@ -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, diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/AlterationEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/AlterationEx.kt index 1409acc..f838630 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/AlterationEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/AlterationEx.kt @@ -13,18 +13,6 @@ fun List.toStatus(): Map> { return status } -fun List.toActiveStatus(): Map> { - val status = hashMapOf>() - forEach { alteration -> - if (alteration.active) { - alteration.status.forEach { - status.getOrPut(it.key) { mutableListOf() }.add(it.value) - } - } - } - return status -} - val List?.sum: Int get() = this?.sumOf { alt -> alt.bonus.sumOf { it.value } } ?: 0 diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/CharacterSheetEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/CharacterSheetEx.kt index d0d6ba3..94921f9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/CharacterSheetEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/local/CharacterSheetEx.kt @@ -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 diff --git a/app/src/main/res/drawable/ic_bloody_stash_24.xml b/app/src/main/res/drawable/ic_bloody_stash_24.xml new file mode 100644 index 0000000..501e534 --- /dev/null +++ b/app/src/main/res/drawable/ic_bloody_stash_24.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_burning_passion_24.xml b/app/src/main/res/drawable/ic_burning_passion_24.xml new file mode 100644 index 0000000..aacda3b --- /dev/null +++ b/app/src/main/res/drawable/ic_burning_passion_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_embrassed_energy_24.xml b/app/src/main/res/drawable/ic_embrassed_energy_24.xml new file mode 100644 index 0000000..2f43d72 --- /dev/null +++ b/app/src/main/res/drawable/ic_embrassed_energy_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_fist_24.xml b/app/src/main/res/drawable/ic_fist_24.xml new file mode 100644 index 0000000..b6724b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_fist_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_layered_armor_24.xml b/app/src/main/res/drawable/ic_layered_armor_24.xml new file mode 100644 index 0000000..d578ecd --- /dev/null +++ b/app/src/main/res/drawable/ic_layered_armor_24.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_lyre_24.xml b/app/src/main/res/drawable/ic_lyre_24.xml new file mode 100644 index 0000000..a3c236a --- /dev/null +++ b/app/src/main/res/drawable/ic_lyre_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7ca7f19..6672c0b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -154,4 +154,10 @@ Source : %1$s Cible : %1$s + + Capacité + Rage + Endurance Implacable + Inspiration Bardique + Conduit Divin \ No newline at end of file diff --git a/app/src/main/res/values/google.xml b/app/src/main/res/values/google.xml index 62c19b9..15f3ec1 100644 --- a/app/src/main/res/values/google.xml +++ b/app/src/main/res/values/google.xml @@ -1,5 +1,6 @@ - AIzaSyBmagVOEyB68tTJ5QMFMzMQZIHG_4XVCOo 62913404482-ergqkjiuvint49q8lm555j21vvb6af7s.apps.googleusercontent.com + AIzaSyBmagVOEyB68tTJ5QMFMzMQZIHG_4XVCOo + https://rp-lexicon-default-rtdb.europe-west1.firebasedatabase.app/ \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf086be..c6b8dca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,7 +75,7 @@ Coordinates Character sheet - HP + Hit Point CA DC Speed @@ -154,4 +154,10 @@ Source: %1$s Target: %1$s + + Skill + Rage + Relentless Endurance + Bardic Inspiration + Divine Conduit \ No newline at end of file