Add realtime database to the APP.
This commit is contained in:
		
							parent
							
								
									e2209bf005
								
							
						
					
					
						commit
						ee81f9082d
					
				
					 50 changed files with 1478 additions and 342 deletions
				
			
		| 
						 | 
				
			
			@ -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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								app/proguard-rules.pro
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								app/proguard-rules.pro
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -21,9 +21,18 @@
 | 
			
		|||
-renamesourcefileattribute SourceFile
 | 
			
		||||
 | 
			
		||||
## --------------- GOOGLE API ---------------
 | 
			
		||||
 | 
			
		||||
-keepclassmembers class * {
 | 
			
		||||
  @com.google.api.client.util.Key <fields>;
 | 
			
		||||
}
 | 
			
		||||
-keep class com.google.api.services.sheets.** { *; }
 | 
			
		||||
-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.** { *; }
 | 
			
		||||
							
								
								
									
										4
									
								
								app/src/debug/res/drawable/ic_half_heart_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/debug/res/drawable/ic_half_heart_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
<vector android:height="24dp" android:viewportHeight="512"
 | 
			
		||||
    android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <path android:fillColor="#000000" android:pathData="M367.84,23.56c-1.25,-0.01 -2.5,0 -3.75,0.03 -39.56,1.01 -80.22,21.15 -108.66,61.94 -35.39,-50.06 -91.96,-68.93 -140.94,-58.9C61.6,37.46 17.57,83.13 19.84,154.19c2.38,74.37 56.07,131.62 109.84,179.97 26.89,24.17 54.03,46.28 75.47,67 21.43,20.72 36.76,40.13 41.31,57.13l9.03,33.69 9,-33.69c3.99,-14.88 19.58,-34.12 41.59,-55.03 22.01,-20.91 50,-43.8 77.47,-68.78 54.94,-49.96 109.17,-108.88 106.9,-180.88 -2.19,-69.7 -45.2,-115.58 -96.75,-127.13 -8.46,-1.9 -17.13,-2.87 -25.88,-2.91zM139.87,42.69c39.62,-0.27 81.75,20.09 107.53,64.75l7.75,13.44L255.16,239.75c-1.48,-0.18 -2.97,-0.28 -4.5,-0.28 -20.71,0 -37.53,16.78 -37.53,37.5 0,20.72 16.82,37.5 37.53,37.5 1.53,0 3.02,-0.1 4.5,-0.28v117.66c-8.78,-15.03 -21.8,-29.43 -37,-44.13 -1.05,-1.01 -2.12,-2.05 -3.19,-3.06 13.68,-2.09 24.19,-13.89 24.19,-28.16 0,-15.74 -12.79,-28.5 -28.53,-28.5 -15.3,0 -27.78,12.05 -28.47,27.19 -13.03,-11.32 -26.6,-22.92 -39.97,-34.94 -52.87,-47.54 -101.56,-101.07 -103.65,-166.66 -2.02,-63.34 34.62,-99.42 79.72,-108.66 7.05,-1.44 14.29,-2.2 21.63,-2.25zM180.62,84.44c-20.71,0 -37.5,16.78 -37.5,37.5 0,4.62 0.85,9.04 2.38,13.12 -0.14,-0 -0.27,0 -0.41,0 -12.15,0 -22,9.85 -22,22 0,12.15 9.85,22 22,22 12.15,0 22,-9.85 22,-22v-0.13c4.2,1.62 8.76,2.5 13.53,2.5 20.72,0 37.5,-16.78 37.5,-37.5 0,-20.72 -16.78,-37.5 -37.5,-37.5zM206.94,168.13c-10.83,0.55 -19.44,9.53 -19.44,20.5 0,11.32 9.18,20.5 20.5,20.5s20.5,-9.18 20.5,-20.5 -9.18,-20.5 -20.5,-20.5c-0.35,0 -0.71,-0.02 -1.06,0zM173.09,220.56c-14.36,0 -26,11.64 -26,26 0,14.36 11.64,26.03 26,26.03s26.03,-11.67 26.03,-26.03c0,-14.36 -11.67,-26 -26.03,-26z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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_"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ import android.accounts.Account
 | 
			
		|||
import android.content.Context
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.derivedStateOf
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import com.google.android.gms.auth.api.identity.SignInCredential
 | 
			
		||||
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
 | 
			
		||||
| 
						 | 
				
			
			@ -17,32 +18,30 @@ import javax.inject.Singleton
 | 
			
		|||
class AuthenticationRepository @Inject constructor(
 | 
			
		||||
    @ApplicationContext private val context: Context,
 | 
			
		||||
) {
 | 
			
		||||
    private val signInCredential = mutableStateOf<SignInCredential?>(null)
 | 
			
		||||
    val isAuthenticated: State<Boolean> = derivedStateOf { signInCredential.value != null }
 | 
			
		||||
 | 
			
		||||
    val credential: GoogleAccountCredential
 | 
			
		||||
        get() {
 | 
			
		||||
            val credential = GoogleAccountCredential
 | 
			
		||||
                .usingOAuth2(context, capabilities)
 | 
			
		||||
                .setBackOff(ExponentialBackOff())
 | 
			
		||||
 | 
			
		||||
            credential.selectedAccount = signInCredential.value?.let {
 | 
			
		||||
                Account(it.id, ACCOUNT_TYPE)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return credential
 | 
			
		||||
        }
 | 
			
		||||
//    private val signInCredential = mutableStateOf<SignInCredential?>(null)
 | 
			
		||||
//    val isAuthenticated: State<Boolean> = derivedStateOf { signInCredential.value != null }
 | 
			
		||||
//
 | 
			
		||||
//    val credential: GoogleAccountCredential by derivedStateOf {
 | 
			
		||||
//        GoogleAccountCredential
 | 
			
		||||
//            .usingOAuth2(context, capabilities)
 | 
			
		||||
//            .setBackOff(ExponentialBackOff())
 | 
			
		||||
//            .also {
 | 
			
		||||
//                it.selectedAccount = signInCredential.value?.let {
 | 
			
		||||
//                    Account(it.id, ACCOUNT_TYPE)
 | 
			
		||||
//                }
 | 
			
		||||
//            }
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    fun updateAuthenticationState(
 | 
			
		||||
        credential: SignInCredential? = null,
 | 
			
		||||
    ) {
 | 
			
		||||
        signInCredential.value = credential
 | 
			
		||||
//        signInCredential.value = credential
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private const val ACCOUNT_TYPE = "google"
 | 
			
		||||
        private val capabilities = listOf(
 | 
			
		||||
            SheetsScopes.SPREADSHEETS_READONLY,
 | 
			
		||||
        )
 | 
			
		||||
//        private const val ACCOUNT_TYPE = "google"
 | 
			
		||||
//        private val capabilities = listOf(
 | 
			
		||||
//            SheetsScopes.SPREADSHEETS_READONLY,
 | 
			
		||||
//        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,89 @@
 | 
			
		|||
package com.pixelized.rplexicon.repository.authentication
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import com.google.firebase.database.DataSnapshot
 | 
			
		||||
import com.google.firebase.database.DatabaseError
 | 
			
		||||
import com.google.firebase.database.ValueEventListener
 | 
			
		||||
import com.google.firebase.database.ktx.database
 | 
			
		||||
import com.google.firebase.ktx.Firebase
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.model.CharacterSheetFire
 | 
			
		||||
import kotlinx.coroutines.cancel
 | 
			
		||||
import kotlinx.coroutines.channels.awaitClose
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.MutableSharedFlow
 | 
			
		||||
import kotlinx.coroutines.flow.SharedFlow
 | 
			
		||||
import kotlinx.coroutines.flow.callbackFlow
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
import javax.inject.Singleton
 | 
			
		||||
 | 
			
		||||
@Singleton
 | 
			
		||||
class FirebaseRepository @Inject constructor(
 | 
			
		||||
    application: Application,
 | 
			
		||||
) {
 | 
			
		||||
    private val database = Firebase.database(
 | 
			
		||||
        url = application.getString(R.string.firebase_realtime_database),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private val _error = MutableSharedFlow<Exception>()
 | 
			
		||||
    val error: SharedFlow<Exception> get() = _error
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        Firebase.database.setPersistenceEnabled(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCharacter(character: String): Flow<CharacterSheetFire> {
 | 
			
		||||
        return callbackFlow {
 | 
			
		||||
            // reference to the node
 | 
			
		||||
            val reference = database.getReference("$PATH_CHARACTERS/$character")
 | 
			
		||||
            // build a register the callback
 | 
			
		||||
            val listener = reference.addValueEventListener(object : ValueEventListener {
 | 
			
		||||
                override fun onDataChange(dataSnapshot: DataSnapshot) {
 | 
			
		||||
                    val value = try {
 | 
			
		||||
                        dataSnapshot.getValue(CharacterSheetFire::class.java)
 | 
			
		||||
                    } catch (exception: Exception) {
 | 
			
		||||
                        _error.tryEmit(exception)
 | 
			
		||||
                        null
 | 
			
		||||
                    }
 | 
			
		||||
                    if (value != null) {
 | 
			
		||||
                        trySend(value)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onCancelled(error: DatabaseError) {
 | 
			
		||||
                    Log.w(TAG, "Failed to read value.", error.toException())
 | 
			
		||||
                    cancel()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            awaitClose {
 | 
			
		||||
                reference.removeEventListener(listener)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setCharacterHitPoint(character: String, value: Int, extra: Int) {
 | 
			
		||||
        val reference = database.getReference(
 | 
			
		||||
            "$PATH_CHARACTERS/$character/${CharacterSheetFire.HIT_POINT}"
 | 
			
		||||
        )
 | 
			
		||||
        reference.setValue(
 | 
			
		||||
            CharacterSheetFire.HitPoint(
 | 
			
		||||
                additional = extra,
 | 
			
		||||
                value = value,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setToken(character: String, token: String, value: Int) {
 | 
			
		||||
        val reference = database.getReference(
 | 
			
		||||
            "$PATH_CHARACTERS/$character/$token"
 | 
			
		||||
        )
 | 
			
		||||
        reference.setValue(value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private const val TAG = "FirebaseRepository"
 | 
			
		||||
 | 
			
		||||
        private const val PATH_CHARACTERS = "Characters"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -26,22 +26,55 @@ class AlterationRepository @Inject constructor(
 | 
			
		|||
    var lastSuccessFullUpdate: Update = Update.INITIAL
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get all [Alteration] for a character
 | 
			
		||||
     * @return a list of alterations.
 | 
			
		||||
     */
 | 
			
		||||
    fun getAlterations(character: String): List<Alteration> {
 | 
			
		||||
        return _assignedAlterations.value[character] ?: emptyList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get [Alteration] for a character that affect at least one [Property]
 | 
			
		||||
     * @param character the character name
 | 
			
		||||
     * @param properties the property list to filter on.
 | 
			
		||||
     * @return a list of alterations.
 | 
			
		||||
     */
 | 
			
		||||
    fun getAlterations(character: String, vararg properties: Property): List<Alteration> {
 | 
			
		||||
        return getAlterations(character = character).filter {
 | 
			
		||||
            it.status.keys.any { key -> properties.contains(key) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check the activation state of a given alteration for given character.
 | 
			
		||||
     * @param character the character name
 | 
			
		||||
     * @param alteration the alteration name
 | 
			
		||||
     * @return true if the alteration is activated otherwise false
 | 
			
		||||
     */
 | 
			
		||||
    fun getStatus(character: String, alteration: String): Boolean {
 | 
			
		||||
        return _status.value[character + alteration]
 | 
			
		||||
            ?: _assignedAlterations.value[character]?.firstOrNull { it.name == alteration }?.active
 | 
			
		||||
            ?: false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get a map of [Property] and [Alteration.Status] for a given player if the alteration is active.
 | 
			
		||||
     * @param character the character name
 | 
			
		||||
     * @return a map of [Property] and [Alteration.Status]
 | 
			
		||||
     */
 | 
			
		||||
    fun getActiveAlterationsStatus(character: String): Map<Property, List<Alteration.Status>> {
 | 
			
		||||
        val status = hashMapOf<Property, MutableList<Alteration.Status>>()
 | 
			
		||||
        _assignedAlterations.value[character]?.forEach { alteration ->
 | 
			
		||||
            if (_status.value[character + alteration.name] ?: alteration.active) {
 | 
			
		||||
                alteration.status.forEach {
 | 
			
		||||
                    status.getOrPut(it.key) { mutableListOf() }.add(it.value)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return status
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun setStatus(character: String, alteration: String, value: Boolean?) {
 | 
			
		||||
        _status.emit(if (value != null) {
 | 
			
		||||
            _status.value.toMutableMap().also { it[character + alteration] = value }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,16 +2,13 @@ package com.pixelized.rplexicon.repository.parser
 | 
			
		|||
 | 
			
		||||
import com.google.api.services.sheets.v4.model.ValueRange
 | 
			
		||||
import com.pixelized.rplexicon.model.CharacterSheet
 | 
			
		||||
import com.pixelized.rplexicon.repository.parser.alteration.CounterParser
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.sheet
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CharacterSheetParser @Inject constructor(
 | 
			
		||||
    private val counterParser: CounterParser,
 | 
			
		||||
) {
 | 
			
		||||
class CharacterSheetParser @Inject constructor() {
 | 
			
		||||
 | 
			
		||||
    @Throws(IncompatibleSheetStructure::class)
 | 
			
		||||
    fun parse(value: ValueRange): Map<String, CharacterSheet> {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,38 +48,21 @@ class CharacterSheetParser @Inject constructor(
 | 
			
		|||
                            proficiency = item.parseInt(MASTERY) ?: 2,
 | 
			
		||||
                            level = item.parseInt(LEVEL) ?: 2,
 | 
			
		||||
                            characterClass = item.parseString(CLASS) ?: "",
 | 
			
		||||
                            hitPoint = item.parseString(HIT_POINT) ?: "1",
 | 
			
		||||
                            maxHitPoint = item.parseString(MAX_HIT_POINT) ?: "1",
 | 
			
		||||
                            lifeDice = item.parseInt(LIFE_DICE) ?: 0,
 | 
			
		||||
                            spell1 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_1)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell2 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_2)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell3 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_3)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell4 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_4)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell5 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_5)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell6 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_6)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell7 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_7)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell8 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_8)
 | 
			
		||||
                            ),
 | 
			
		||||
                            spell9 = counterParser.parseCounter(
 | 
			
		||||
                                value = item.parseString(SPELL_LEVEL_9)
 | 
			
		||||
                            ),
 | 
			
		||||
                            hitPoint = item.parseInt(MAX_HIT_POINT) ?: 1,
 | 
			
		||||
                            rage = item.parseInt(RAGE),
 | 
			
		||||
                            relentlessEndurance = item.parseInt(RELENTLESS_ENDURANCE),
 | 
			
		||||
                            divineConduit = item.parseInt(DIVINE_CONDUIT),
 | 
			
		||||
                            bardicInspiration = item.parseInt(BARDIC_INSPIRATION),
 | 
			
		||||
                            spell1 = item.parseInt(SPELL_LEVEL_1),
 | 
			
		||||
                            spell2 = item.parseInt(SPELL_LEVEL_2),
 | 
			
		||||
                            spell3 = item.parseInt(SPELL_LEVEL_3),
 | 
			
		||||
                            spell4 = item.parseInt(SPELL_LEVEL_4),
 | 
			
		||||
                            spell5 = item.parseInt(SPELL_LEVEL_5),
 | 
			
		||||
                            spell6 = item.parseInt(SPELL_LEVEL_6),
 | 
			
		||||
                            spell7 = item.parseInt(SPELL_LEVEL_7),
 | 
			
		||||
                            spell8 = item.parseInt(SPELL_LEVEL_8),
 | 
			
		||||
                            spell9 = item.parseInt(SPELL_LEVEL_9),
 | 
			
		||||
                            dC = item.parseInt(DD_SAVE_THROW),
 | 
			
		||||
                            criticalModifier = item.parseInt(CRITICAL_MODIFIER) ?: 2,
 | 
			
		||||
                            armorClass = item.parseInt(ARMOR_CLASS) ?: 10,
 | 
			
		||||
                            speed = item.parseInt(SPEED) ?: 10,
 | 
			
		||||
                            strength = item.parseInt(STRENGTH) ?: 10,
 | 
			
		||||
| 
						 | 
				
			
			@ -131,9 +111,11 @@ class CharacterSheetParser @Inject constructor(
 | 
			
		|||
        private const val RACE = "Race"
 | 
			
		||||
        private const val LEVEL = "Niveau"
 | 
			
		||||
        private const val CLASS = "Classe"
 | 
			
		||||
        private const val HIT_POINT = "Point de vie"
 | 
			
		||||
        private const val MAX_HIT_POINT = "Point de vie MAX"
 | 
			
		||||
        private const val LIFE_DICE = "Dé de vie"
 | 
			
		||||
        private const val RAGE =  "Rage"
 | 
			
		||||
        private const val RELENTLESS_ENDURANCE = "Endurance implacable"
 | 
			
		||||
        private const val DIVINE_CONDUIT = "Conduit divin"
 | 
			
		||||
        private const val BARDIC_INSPIRATION = "Inspiration bardique"
 | 
			
		||||
        private const val SPELL_LEVEL_1 = "Sort de niveau 1"
 | 
			
		||||
        private const val SPELL_LEVEL_2 = "Sort de niveau 2"
 | 
			
		||||
        private const val SPELL_LEVEL_3 = "Sort de niveau 3"
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +126,6 @@ class CharacterSheetParser @Inject constructor(
 | 
			
		|||
        private const val SPELL_LEVEL_8 = "Sort de niveau 8"
 | 
			
		||||
        private const val SPELL_LEVEL_9 = "Sort de niveau 9"
 | 
			
		||||
        private const val DD_SAVE_THROW = "DD sauvergarde des sorts"
 | 
			
		||||
        private const val CRITICAL_MODIFIER = "Dé de critique"
 | 
			
		||||
        private const val ARMOR_CLASS = "Classe d'armure"
 | 
			
		||||
        private const val SPEED = "Vitesse"
 | 
			
		||||
        private const val MASTERY = "Bonus de maîtrise"
 | 
			
		||||
| 
						 | 
				
			
			@ -185,9 +166,11 @@ class CharacterSheetParser @Inject constructor(
 | 
			
		|||
                RACE,
 | 
			
		||||
                LEVEL,
 | 
			
		||||
                CLASS,
 | 
			
		||||
                HIT_POINT,
 | 
			
		||||
                MAX_HIT_POINT,
 | 
			
		||||
                LIFE_DICE,
 | 
			
		||||
                RAGE,
 | 
			
		||||
                RELENTLESS_ENDURANCE,
 | 
			
		||||
                DIVINE_CONDUIT,
 | 
			
		||||
                BARDIC_INSPIRATION,
 | 
			
		||||
                SPELL_LEVEL_1,
 | 
			
		||||
                SPELL_LEVEL_2,
 | 
			
		||||
                SPELL_LEVEL_3,
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +181,6 @@ class CharacterSheetParser @Inject constructor(
 | 
			
		|||
                SPELL_LEVEL_8,
 | 
			
		||||
                SPELL_LEVEL_9,
 | 
			
		||||
                DD_SAVE_THROW,
 | 
			
		||||
                CRITICAL_MODIFIER,
 | 
			
		||||
                ARMOR_CLASS,
 | 
			
		||||
                SPEED,
 | 
			
		||||
                MASTERY,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 }
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,84 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.composable.edit
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.ExperimentalFoundationApi
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.pager.rememberPagerState
 | 
			
		||||
import androidx.compose.foundation.shape.CutCornerShape
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Surface
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.TextButton
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.window.Dialog
 | 
			
		||||
import androidx.compose.ui.window.DialogProperties
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.NumberPicker
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class CounterEditDialogUio(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    val label: String,
 | 
			
		||||
    val value: Int,
 | 
			
		||||
    val max: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalFoundationApi::class)
 | 
			
		||||
@Composable
 | 
			
		||||
fun HandleCounterEditDialog(
 | 
			
		||||
    dialog: State<CounterEditDialogUio?>,
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    onConfirm: (String, Int) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    dialog.value?.let {
 | 
			
		||||
        val pager = rememberPagerState(initialPage = it.value) { it.max + 1 }
 | 
			
		||||
        Dialog(
 | 
			
		||||
            properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
 | 
			
		||||
            onDismissRequest = onDismissRequest,
 | 
			
		||||
        ) {
 | 
			
		||||
            Surface(
 | 
			
		||||
                modifier = Modifier.ddBorder(
 | 
			
		||||
                    inner = remember { RoundedCornerShape(size = 8.dp) },
 | 
			
		||||
                    outline = remember { CutCornerShape(size = 16.dp) },
 | 
			
		||||
                )
 | 
			
		||||
            ) {
 | 
			
		||||
                Column(
 | 
			
		||||
                    horizontalAlignment = Alignment.CenterHorizontally,
 | 
			
		||||
                ) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
 | 
			
		||||
                        style = MaterialTheme.typography.labelSmall,
 | 
			
		||||
                        text = it.label,
 | 
			
		||||
                    )
 | 
			
		||||
                    Box(
 | 
			
		||||
                        modifier = Modifier
 | 
			
		||||
                            .size(width = 64.dp, height = 1.dp)
 | 
			
		||||
                            .background(color = MaterialTheme.lexicon.colorScheme.characterSheet.innerBorder),
 | 
			
		||||
                    )
 | 
			
		||||
                    NumberPicker(
 | 
			
		||||
                        modifier = Modifier.width(width = 128.dp),
 | 
			
		||||
                        pager = pager,
 | 
			
		||||
                    )
 | 
			
		||||
                    TextButton(
 | 
			
		||||
                        onClick = { onConfirm(it.id, pager.currentPage) },
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Text(text = stringResource(id = android.R.string.ok))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,129 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.composable.edit
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
 | 
			
		||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
 | 
			
		||||
import androidx.compose.foundation.ExperimentalFoundationApi
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.pager.rememberPagerState
 | 
			
		||||
import androidx.compose.foundation.shape.CutCornerShape
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Surface
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.TextButton
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.window.Dialog
 | 
			
		||||
import androidx.compose.ui.window.DialogProperties
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.NumberPicker
 | 
			
		||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class HpPointDialogUio(
 | 
			
		||||
    val value: Int,
 | 
			
		||||
    val max: Int,
 | 
			
		||||
    val extra: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalFoundationApi::class)
 | 
			
		||||
@Composable
 | 
			
		||||
fun HandleHitPointEditDialog(
 | 
			
		||||
    dialog: State<HpPointDialogUio?>,
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    onConfirm: (hp: Int, extra: Int) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    dialog.value?.let {
 | 
			
		||||
        val hpPager = rememberPagerState(initialPage = it.value) { it.max + 1 }
 | 
			
		||||
        val extraPager = rememberPagerState(initialPage = it.extra) { 99 }
 | 
			
		||||
        Dialog(
 | 
			
		||||
            properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
 | 
			
		||||
            onDismissRequest = onDismissRequest,
 | 
			
		||||
        ) {
 | 
			
		||||
            Surface(
 | 
			
		||||
                modifier = Modifier.ddBorder(
 | 
			
		||||
                    inner = remember { RoundedCornerShape(size = 8.dp) },
 | 
			
		||||
                    outline = remember { CutCornerShape(size = 16.dp) },
 | 
			
		||||
                )
 | 
			
		||||
            ) {
 | 
			
		||||
                Column(
 | 
			
		||||
                    horizontalAlignment = Alignment.CenterHorizontally,
 | 
			
		||||
                ) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        modifier = Modifier.padding(vertical = 4.dp),
 | 
			
		||||
                        style = MaterialTheme.typography.labelSmall,
 | 
			
		||||
                        text = stringResource(id = R.string.character_sheet_title_hp),
 | 
			
		||||
                    )
 | 
			
		||||
                    Box(
 | 
			
		||||
                        modifier = Modifier
 | 
			
		||||
                            .size(width = 64.dp, height = 1.dp)
 | 
			
		||||
                            .background(color = MaterialTheme.lexicon.colorScheme.characterSheet.innerBorder),
 | 
			
		||||
                    )
 | 
			
		||||
                    Row(
 | 
			
		||||
                        verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                    ) {
 | 
			
		||||
                        NumberPicker(
 | 
			
		||||
                            modifier = Modifier.width(width = 64.dp),
 | 
			
		||||
                            pager = hpPager,
 | 
			
		||||
                        )
 | 
			
		||||
                        Text(
 | 
			
		||||
                            fontWeight = FontWeight.Light,
 | 
			
		||||
                            style = MaterialTheme.typography.bodyLarge,
 | 
			
		||||
                            text = "+"
 | 
			
		||||
                        )
 | 
			
		||||
                        NumberPicker(
 | 
			
		||||
                            modifier = Modifier.width(width = 64.dp),
 | 
			
		||||
                            pager = extraPager,
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    TextButton(
 | 
			
		||||
                        onClick = { onConfirm(hpPager.currentPage, extraPager.currentPage) },
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Text(text = stringResource(id = android.R.string.ok))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
 | 
			
		||||
@Preview(uiMode = UI_MODE_NIGHT_YES)
 | 
			
		||||
private fun HandleHitPointEditDialogPreview() {
 | 
			
		||||
    LexiconTheme {
 | 
			
		||||
        Surface {
 | 
			
		||||
            val dialog = remember {
 | 
			
		||||
                mutableStateOf(
 | 
			
		||||
                    HpPointDialogUio(
 | 
			
		||||
                        value = 20,
 | 
			
		||||
                        max = 30,
 | 
			
		||||
                        extra = 0,
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            HandleHitPointEditDialog(
 | 
			
		||||
                dialog = dialog,
 | 
			
		||||
                onDismissRequest = { },
 | 
			
		||||
                onConfirm = { _, _ -> },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import androidx.navigation.NavHostController
 | 
			
		|||
import androidx.navigation.NavOptionsBuilder
 | 
			
		||||
import androidx.navigation.compose.NavHost
 | 
			
		||||
import androidx.navigation.compose.rememberNavController
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.HOME_ROUTE
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterSheet
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +27,7 @@ val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> {
 | 
			
		|||
@Composable
 | 
			
		||||
fun ScreenNavHost(
 | 
			
		||||
    navHostController: NavHostController = rememberNavController(),
 | 
			
		||||
    startDestination: String = HOME_ROUTE,
 | 
			
		||||
    startDestination: String = AUTHENTICATION_ROUTE,
 | 
			
		||||
) {
 | 
			
		||||
    val lexiconListState = rememberLazyListState()
 | 
			
		||||
    val questListState = rememberLazyListState()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.height
 | 
			
		|||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.systemBarsPadding
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.material3.CircularProgressIndicator
 | 
			
		||||
import androidx.compose.material3.LocalTextStyle
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.OutlinedButton
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +50,7 @@ import androidx.compose.ui.text.withStyle
 | 
			
		|||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.window.Dialog
 | 
			
		||||
import androidx.hilt.navigation.compose.hiltViewModel
 | 
			
		||||
import com.pixelized.rplexicon.LocalActivity
 | 
			
		||||
import com.pixelized.rplexicon.LocalSnack
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +72,9 @@ sealed class AuthenticationStateUio {
 | 
			
		|||
    @Stable
 | 
			
		||||
    data object Initial : AuthenticationStateUio()
 | 
			
		||||
 | 
			
		||||
    @Stable
 | 
			
		||||
    data object Progress : AuthenticationStateUio()
 | 
			
		||||
 | 
			
		||||
    @Stable
 | 
			
		||||
    data object Success : AuthenticationStateUio()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -104,6 +109,11 @@ fun AuthenticationScreen(
 | 
			
		|||
 | 
			
		||||
        HandleAuthenticationState(
 | 
			
		||||
            state = state,
 | 
			
		||||
            onProgress = {
 | 
			
		||||
                Dialog(onDismissRequest = { }) {
 | 
			
		||||
                    CircularProgressIndicator()
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            onSignIn = {
 | 
			
		||||
                screen.navigateToHome(option = rootOption())
 | 
			
		||||
            },
 | 
			
		||||
| 
						 | 
				
			
			@ -119,20 +129,24 @@ fun AuthenticationScreen(
 | 
			
		|||
@Composable
 | 
			
		||||
fun HandleAuthenticationState(
 | 
			
		||||
    state: State<AuthenticationStateUio>,
 | 
			
		||||
    onProgress: @Composable () -> Unit,
 | 
			
		||||
    onSignIn: suspend CoroutineScope.() -> Unit,
 | 
			
		||||
    onSignInError: suspend CoroutineScope.(exception: Exception?) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    when (val dummy = state.value) {
 | 
			
		||||
        AuthenticationStateUio.Initial -> Unit
 | 
			
		||||
        is AuthenticationStateUio.Success -> {
 | 
			
		||||
            LaunchedEffect(key1 = "Authentication.Success", block = onSignIn)
 | 
			
		||||
        }
 | 
			
		||||
        is AuthenticationStateUio.Initial -> Unit
 | 
			
		||||
 | 
			
		||||
        is AuthenticationStateUio.Failure -> {
 | 
			
		||||
            LaunchedEffect(key1 = "Authentication.Error") {
 | 
			
		||||
                onSignInError(dummy.exception)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        is AuthenticationStateUio.Progress -> onProgress()
 | 
			
		||||
 | 
			
		||||
        is AuthenticationStateUio.Success -> LaunchedEffect(
 | 
			
		||||
            key1 = "Authentication.Success",
 | 
			
		||||
            block = onSignIn,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        is AuthenticationStateUio.Failure -> LaunchedEffect(
 | 
			
		||||
            key1 = "Authentication.Error",
 | 
			
		||||
            block = { onSignInError(dummy.exception) },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,9 @@ import androidx.lifecycle.AndroidViewModel
 | 
			
		|||
import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
 | 
			
		||||
import com.google.android.gms.auth.api.identity.Identity
 | 
			
		||||
import com.google.android.gms.auth.api.identity.SignInCredential
 | 
			
		||||
import com.google.firebase.auth.GoogleAuthProvider
 | 
			
		||||
import com.google.firebase.auth.ktx.auth
 | 
			
		||||
import com.google.firebase.ktx.Firebase
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.repository.authentication.AuthenticationRepository
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
| 
						 | 
				
			
			@ -30,11 +33,12 @@ class AuthenticationViewModel @Inject constructor(
 | 
			
		|||
    private val context: Context get() = getApplication()
 | 
			
		||||
    private var launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>? = null
 | 
			
		||||
 | 
			
		||||
    private val state = mutableStateOf(
 | 
			
		||||
        when (repository.isAuthenticated.value) {
 | 
			
		||||
            true -> AuthenticationStateUio.Success
 | 
			
		||||
            else -> AuthenticationStateUio.Initial
 | 
			
		||||
        }
 | 
			
		||||
    private val state = mutableStateOf<AuthenticationStateUio>(
 | 
			
		||||
        AuthenticationStateUio.Initial
 | 
			
		||||
//        when (repository.isAuthenticated.value) {
 | 
			
		||||
//            true -> AuthenticationStateUio.Success
 | 
			
		||||
//            else -> AuthenticationStateUio.Initial
 | 
			
		||||
//        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
| 
						 | 
				
			
			@ -43,13 +47,28 @@ class AuthenticationViewModel @Inject constructor(
 | 
			
		|||
            contract = ActivityResultContracts.StartIntentSenderForResult(),
 | 
			
		||||
            onResult = {
 | 
			
		||||
                if (it.resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                    val credential: SignInCredential = Identity
 | 
			
		||||
                    state.value = AuthenticationStateUio.Progress
 | 
			
		||||
                    // sign in request succeed. retrieve google credential
 | 
			
		||||
                    val googleCredential: SignInCredential = Identity
 | 
			
		||||
                        .getSignInClient(context)
 | 
			
		||||
                        .getSignInCredentialFromIntent(it.data)
 | 
			
		||||
 | 
			
		||||
                    state.value = AuthenticationStateUio.Success
 | 
			
		||||
                    repository.updateAuthenticationState(credential = credential)
 | 
			
		||||
                    // build firebase credential
 | 
			
		||||
                    val firebaseCredential = GoogleAuthProvider
 | 
			
		||||
                        .getCredential(googleCredential.googleIdToken, null)
 | 
			
		||||
                    // sign in to Firebase
 | 
			
		||||
                    Firebase.auth
 | 
			
		||||
                        .signInWithCredential(firebaseCredential)
 | 
			
		||||
                        .addOnCompleteListener { task ->
 | 
			
		||||
                            if (task.isSuccessful) {
 | 
			
		||||
                                state.value = AuthenticationStateUio.Success
 | 
			
		||||
                                repository.updateAuthenticationState(credential = googleCredential)
 | 
			
		||||
                            } else {
 | 
			
		||||
                                state.value = AuthenticationStateUio.Failure(task.exception)
 | 
			
		||||
                                repository.updateAuthenticationState(credential = null)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                } else {
 | 
			
		||||
                    state.value = AuthenticationStateUio.Initial
 | 
			
		||||
                    repository.updateAuthenticationState(credential = null)
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
| 
						 | 
				
			
			@ -60,10 +79,13 @@ class AuthenticationViewModel @Inject constructor(
 | 
			
		|||
    fun signIn(activity: Activity) {
 | 
			
		||||
        state.value = AuthenticationStateUio.Initial
 | 
			
		||||
 | 
			
		||||
        // build a request to sign in with google credential.
 | 
			
		||||
        // At that point we do only use google sign in service
 | 
			
		||||
        val request: GetSignInIntentRequest = GetSignInIntentRequest.builder()
 | 
			
		||||
            .setServerClientId(context.getString(R.string.google_sign_in_id))
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
        // use the pre register launcher to start the sign in request intent.
 | 
			
		||||
        Identity.getSignInClient(activity).getSignInIntent(request)
 | 
			
		||||
            .addOnSuccessListener { result ->
 | 
			
		||||
                try {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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() },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,60 +3,31 @@ package com.pixelized.rplexicon.ui.screens.character
 | 
			
		|||
import android.util.Log
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.lifecycle.SavedStateHandle
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.ActionRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.AlterationRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.SpellRepository
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.factory.CharacterSheetHeaderUioFactory
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.local.toActiveStatus
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.async
 | 
			
		||||
import kotlinx.coroutines.awaitAll
 | 
			
		||||
import kotlinx.coroutines.coroutineScope
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
class CharacterSheetViewModel @Inject constructor(
 | 
			
		||||
    savedStateHandle: SavedStateHandle,
 | 
			
		||||
    private val characterRepository: CharacterSheetRepository,
 | 
			
		||||
    private val alterationRepository: AlterationRepository,
 | 
			
		||||
    private val actionRepository: ActionRepository,
 | 
			
		||||
    private val spellRepository: SpellRepository,
 | 
			
		||||
    headerFactory: CharacterSheetHeaderUioFactory,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
    val character = savedStateHandle.characterSheetArgument.name
 | 
			
		||||
 | 
			
		||||
    private val _header = mutableStateOf<CharacterSheetHeaderUio?>(null)
 | 
			
		||||
    val header: State<CharacterSheetHeaderUio?> get() = _header
 | 
			
		||||
 | 
			
		||||
    private val _isLoading = mutableStateOf(false)
 | 
			
		||||
    val isLoading: State<Boolean> get() = _isLoading
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            launch {
 | 
			
		||||
                characterRepository.data
 | 
			
		||||
                    .combine(alterationRepository.assignedAlterations) { sheets, _ -> sheets }
 | 
			
		||||
                    .collect { sheets ->
 | 
			
		||||
                        _header.value = withContext(Dispatchers.Default) {
 | 
			
		||||
                            val alterations =
 | 
			
		||||
                                alterationRepository.getAlterations(character = character)
 | 
			
		||||
                            headerFactory.convert(
 | 
			
		||||
                                model = sheets.getValue(character),
 | 
			
		||||
                                alterations = alterations.toActiveStatus(),
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                update(force = false)
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = { },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = { _, _, _ -> },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,14 +2,12 @@ package com.pixelized.rplexicon.ui.screens.character.composable.character
 | 
			
		|||
 | 
			
		||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
 | 
			
		||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
 | 
			
		||||
import androidx.compose.foundation.ExperimentalFoundationApi
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.pager.PagerState
 | 
			
		||||
import androidx.compose.foundation.pager.rememberPagerState
 | 
			
		||||
import androidx.compose.material3.Divider
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Surface
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
| 
						 | 
				
			
			@ -19,9 +17,9 @@ import androidx.compose.ui.Alignment
 | 
			
		|||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.IndicatorStep
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
 | 
			
		||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class CharacterSheetHeaderUio(
 | 
			
		||||
| 
						 | 
				
			
			@ -31,55 +29,58 @@ data class CharacterSheetHeaderUio(
 | 
			
		|||
    val dC: LabelPointUio?,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalFoundationApi::class)
 | 
			
		||||
@Composable
 | 
			
		||||
fun CharacterSheetHeader(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    pagerState: PagerState,
 | 
			
		||||
    padding : PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
 | 
			
		||||
    header: State<CharacterSheetHeaderUio?>,
 | 
			
		||||
    onHitPoint : () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Surface(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        shadowElevation = 4.dp,
 | 
			
		||||
    ) {
 | 
			
		||||
        Column(
 | 
			
		||||
            horizontalAlignment = Alignment.CenterHorizontally,
 | 
			
		||||
        ) {
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .padding(horizontal = 16.dp),
 | 
			
		||||
                modifier = Modifier.padding(padding),
 | 
			
		||||
                verticalAlignment = Alignment.Bottom,
 | 
			
		||||
                horizontalArrangement = Arrangement.spacedBy(
 | 
			
		||||
                    space = 16.dp,
 | 
			
		||||
                    alignment = Alignment.CenterHorizontally
 | 
			
		||||
                ),
 | 
			
		||||
            ) {
 | 
			
		||||
                header.value?.armorClass?.let { LabelPoint(label = it) }
 | 
			
		||||
                header.value?.hitPoint?.let { LabelPoint(label = it) }
 | 
			
		||||
                header.value?.dC?.let { LabelPoint(label = it) }
 | 
			
		||||
                header.value?.speed?.let { LabelPoint(label = it,) }
 | 
			
		||||
                header.value?.armorClass?.let {
 | 
			
		||||
                    LabelPoint(label = it)
 | 
			
		||||
                }
 | 
			
		||||
                header.value?.hitPoint?.let {
 | 
			
		||||
                    LabelPoint(label = it, onClick = onHitPoint)
 | 
			
		||||
                }
 | 
			
		||||
                header.value?.dC?.let {
 | 
			
		||||
                    LabelPoint(label = it)
 | 
			
		||||
                }
 | 
			
		||||
                header.value?.speed?.let {
 | 
			
		||||
                    LabelPoint(label = it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            IndicatorStep(
 | 
			
		||||
                modifier = Modifier.padding(vertical = 4.dp),
 | 
			
		||||
                count = pagerState.pageCount,
 | 
			
		||||
                selectedIndex = pagerState.currentPage,
 | 
			
		||||
            Divider(
 | 
			
		||||
                modifier = Modifier.padding(horizontal = 16.dp),
 | 
			
		||||
                color = MaterialTheme.lexicon.colorScheme.placeholder,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalFoundationApi::class)
 | 
			
		||||
@Composable
 | 
			
		||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
 | 
			
		||||
@Preview(uiMode = UI_MODE_NIGHT_YES)
 | 
			
		||||
fun CharacterSheetHeaderPreview() {
 | 
			
		||||
private fun CharacterSheetHeaderPreview() {
 | 
			
		||||
    LexiconTheme {
 | 
			
		||||
        Surface {
 | 
			
		||||
            CharacterSheetHeader(
 | 
			
		||||
                header = rememberCharacterHeaderStatePreview(),
 | 
			
		||||
                pagerState = rememberPagerState { 2 },
 | 
			
		||||
                onHitPoint = { },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,42 +1,34 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.screens.character.factory
 | 
			
		||||
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.model.Alteration
 | 
			
		||||
import com.pixelized.rplexicon.model.Attack
 | 
			
		||||
import com.pixelized.rplexicon.model.CharacterSheet
 | 
			
		||||
import com.pixelized.rplexicon.model.Property
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.character.LabelPointUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.pages.actions.HeaderViewModel
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class CharacterSheetHeaderUioFactory @Inject constructor() {
 | 
			
		||||
 | 
			
		||||
    fun convert(
 | 
			
		||||
        model: CharacterSheet,
 | 
			
		||||
        alterations: Map<Property, List<Alteration.Status>>,
 | 
			
		||||
        sheetHeaderData: HeaderViewModel.SheetHeaderData?,
 | 
			
		||||
        fireHeaderData: HeaderViewModel.FireHeaderData?,
 | 
			
		||||
    ): CharacterSheetHeaderUio {
 | 
			
		||||
        // compute alteration for the CA.
 | 
			
		||||
        val armorClassAlteration: Int = alterations[Property.ARMOR_CLASS]?.sumOf {
 | 
			
		||||
            it.bonus.sumOf { bonus -> bonus.value }
 | 
			
		||||
        } ?: 0
 | 
			
		||||
 | 
			
		||||
        return CharacterSheetHeaderUio(
 | 
			
		||||
            armorClass = LabelPointUio(
 | 
			
		||||
                label = R.string.character_sheet_title_ca,
 | 
			
		||||
                value = "${model.armorClass + armorClassAlteration}",
 | 
			
		||||
                value = sheetHeaderData?.ca?.let { "$it" } ?: "  ",
 | 
			
		||||
                max = null,
 | 
			
		||||
            ),
 | 
			
		||||
            speed = LabelPointUio(
 | 
			
		||||
                label = R.string.character_sheet_title_speed,
 | 
			
		||||
                value = "${model.speed}",
 | 
			
		||||
                value = sheetHeaderData?.speed?.let { "$it" } ?: "  ",
 | 
			
		||||
                max = "m",
 | 
			
		||||
            ),
 | 
			
		||||
            hitPoint = LabelPointUio(
 | 
			
		||||
                label = R.string.character_sheet_title_hp,
 | 
			
		||||
                value = model.hitPoint,
 | 
			
		||||
                max = "/ ${model.maxHitPoint}",
 | 
			
		||||
                value = convertToHitPointLabel(hitPoint = fireHeaderData),
 | 
			
		||||
                max = sheetHeaderData?.hpMax?.let { "/ $it" } ?: "  ",
 | 
			
		||||
            ),
 | 
			
		||||
            dC = model.dC?.let {
 | 
			
		||||
            dC = sheetHeaderData?.dc?.let {
 | 
			
		||||
                LabelPointUio(
 | 
			
		||||
                    label = R.string.character_sheet_title_dc,
 | 
			
		||||
                    value = "$it",
 | 
			
		||||
| 
						 | 
				
			
			@ -44,4 +36,12 @@ class CharacterSheetHeaderUioFactory @Inject constructor() {
 | 
			
		|||
            }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun convertToHitPointLabel(hitPoint: HeaderViewModel.FireHeaderData?): String {
 | 
			
		||||
        return when {
 | 
			
		||||
            hitPoint?.hp == null -> "?"
 | 
			
		||||
            hitPoint.extra == 0 -> "${hitPoint.hp}"
 | 
			
		||||
            else -> "${hitPoint.hp}+${hitPoint.extra}"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,8 +4,10 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
 | 
			
		|||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
 | 
			
		||||
import androidx.compose.foundation.ExperimentalFoundationApi
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Spacer
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyColumn
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
| 
						 | 
				
			
			@ -13,15 +15,22 @@ import androidx.compose.foundation.lazy.items
 | 
			
		|||
import androidx.compose.foundation.lazy.rememberLazyListState
 | 
			
		||||
import androidx.compose.material.ExperimentalMaterialApi
 | 
			
		||||
import androidx.compose.material.ModalBottomSheetState
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Surface
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.AnnotatedString
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.hilt.navigation.compose.hiltViewModel
 | 
			
		||||
import com.pixelized.rplexicon.LocalRollOverlay
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.edit.HandleCounterEditDialog
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSpellDetail
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.actions.Attack
 | 
			
		||||
| 
						 | 
				
			
			@ -30,18 +39,26 @@ import com.pixelized.rplexicon.ui.screens.character.composable.actions.Spell
 | 
			
		|||
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeader
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeaderUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberAttackListStatePreview
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberSpellListStatePreview
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItem
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItemUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.rememberTokenListStatePreview
 | 
			
		||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalMaterialApi::class)
 | 
			
		||||
@Composable
 | 
			
		||||
fun ActionPage(
 | 
			
		||||
    character: String,
 | 
			
		||||
    sheetState: ModalBottomSheetState,
 | 
			
		||||
    headerViewModel: HeaderViewModel = hiltViewModel(),
 | 
			
		||||
    attackViewModel: AttackActionViewModel = hiltViewModel(),
 | 
			
		||||
    spellsViewModel: SpellsActionViewModel = hiltViewModel(),
 | 
			
		||||
    tokenViewModel: TokenViewModel = hiltViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val screen = LocalScreenNavHost.current
 | 
			
		||||
    val overlay = LocalRollOverlay.current
 | 
			
		||||
| 
						 | 
				
			
			@ -49,8 +66,11 @@ fun ActionPage(
 | 
			
		|||
 | 
			
		||||
    ActionsPageContent(
 | 
			
		||||
        modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
        header = headerViewModel.header,
 | 
			
		||||
        attacks = attackViewModel.attacks,
 | 
			
		||||
        tokens = tokenViewModel.tokens,
 | 
			
		||||
        spells = spellsViewModel.spells,
 | 
			
		||||
        onHitPoint = headerViewModel::toggleHitPointDialog,
 | 
			
		||||
        onAttackHit = { id ->
 | 
			
		||||
            attackViewModel.onHitRoll(id)?.let {
 | 
			
		||||
                overlay.prepareRoll(diceThrow = it)
 | 
			
		||||
| 
						 | 
				
			
			@ -63,9 +83,15 @@ fun ActionPage(
 | 
			
		|||
                overlay.showOverlay()
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        onToken = {
 | 
			
		||||
            tokenViewModel.showTokenEditDialog(dialog = it)
 | 
			
		||||
        },
 | 
			
		||||
        onSpellLevel = { level: Int, value: Int, max: Int ->
 | 
			
		||||
            tokenViewModel.showSpellTokenEditDialog(level = level, value = value, max = max)
 | 
			
		||||
        },
 | 
			
		||||
        onSpell = { spell ->
 | 
			
		||||
            screen.navigateToSpellDetail(
 | 
			
		||||
                character = character,
 | 
			
		||||
                character = headerViewModel.character,
 | 
			
		||||
                spell = spell,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +113,18 @@ fun ActionPage(
 | 
			
		|||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    HandleHitPointEditDialog(
 | 
			
		||||
        dialog = headerViewModel.hitPointDialog,
 | 
			
		||||
        onDismissRequest = headerViewModel::toggleHitPointDialog,
 | 
			
		||||
        onConfirm = headerViewModel::applyHitPointChange,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    HandleCounterEditDialog(
 | 
			
		||||
        dialog = tokenViewModel.dialog,
 | 
			
		||||
        onDismissRequest = tokenViewModel::hideCounterEditDialog,
 | 
			
		||||
        onConfirm = tokenViewModel::applyCounterValue
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalFoundationApi::class)
 | 
			
		||||
| 
						 | 
				
			
			@ -94,10 +132,15 @@ fun ActionPage(
 | 
			
		|||
fun ActionsPageContent(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    lazyListState: LazyListState = rememberLazyListState(),
 | 
			
		||||
    header: State<CharacterSheetHeaderUio?>,
 | 
			
		||||
    attacks: State<List<AttackUio>>,
 | 
			
		||||
    tokens: State<List<TokenItemUio>>,
 | 
			
		||||
    spells: State<List<Pair<SpellHeaderUio, List<SpellUio>>>>,
 | 
			
		||||
    onHitPoint: () -> Unit,
 | 
			
		||||
    onAttackHit: (id: String) -> Unit,
 | 
			
		||||
    onAttackDamage: (id: String) -> Unit,
 | 
			
		||||
    onToken: (TokenItemUio) -> Unit,
 | 
			
		||||
    onSpellLevel: (level: Int, value: Int, max: Int) -> Unit,
 | 
			
		||||
    onSpell: (id: String) -> Unit,
 | 
			
		||||
    onSpellHit: (id: String) -> Unit,
 | 
			
		||||
    onSpellDamage: (id: String) -> Unit,
 | 
			
		||||
| 
						 | 
				
			
			@ -106,28 +149,73 @@ fun ActionsPageContent(
 | 
			
		|||
    LazyColumn(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        state = lazyListState,
 | 
			
		||||
        contentPadding = PaddingValues(vertical = 8.dp),
 | 
			
		||||
        contentPadding = PaddingValues(bottom = 8.dp),
 | 
			
		||||
    ) {
 | 
			
		||||
        stickyHeader {
 | 
			
		||||
            CharacterSheetHeader(
 | 
			
		||||
                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                header = header,
 | 
			
		||||
                onHitPoint = onHitPoint,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(count = 1) {
 | 
			
		||||
            Spacer(modifier = Modifier.height(height = 8.dp))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(items = attacks.value) {
 | 
			
		||||
            Attack(
 | 
			
		||||
                modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
 | 
			
		||||
                weapon = it,
 | 
			
		||||
                onHit = onAttackHit,
 | 
			
		||||
                onDamage = onAttackDamage,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        spells.value.forEach { entry ->
 | 
			
		||||
        if (tokens.value.isNotEmpty()) {
 | 
			
		||||
            items(count = 1) {
 | 
			
		||||
                Spacer(modifier = Modifier.height(height = 16.dp))
 | 
			
		||||
            }
 | 
			
		||||
            stickyHeader {
 | 
			
		||||
                SpellHeader(
 | 
			
		||||
                    modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
 | 
			
		||||
                    header = entry.first,
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.padding(horizontal = 16.dp),
 | 
			
		||||
                    style = MaterialTheme.typography.titleMedium,
 | 
			
		||||
                    text = stringResource(id = R.string.token_label_title).let {
 | 
			
		||||
                        AnnotatedString(
 | 
			
		||||
                            text = it,
 | 
			
		||||
                            spanStyles = listOf(
 | 
			
		||||
                                AnnotatedString.Range(
 | 
			
		||||
                                    item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
 | 
			
		||||
                                    start = 0,
 | 
			
		||||
                                    end = Integer.min(1, it.length),
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            items(items = tokens.value) {
 | 
			
		||||
                TokenItem(
 | 
			
		||||
                    counter = it,
 | 
			
		||||
                    onClick = onToken,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        spells.value.forEach { entry ->
 | 
			
		||||
            items(count = 1) {
 | 
			
		||||
                Spacer(modifier = Modifier.height(height = 8.dp))
 | 
			
		||||
            }
 | 
			
		||||
            stickyHeader {
 | 
			
		||||
                SpellHeader(
 | 
			
		||||
                    header = entry.first,
 | 
			
		||||
                    onSpell = onSpellLevel,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            items(count = 1) {
 | 
			
		||||
                Spacer(modifier = Modifier.height(height = 8.dp))
 | 
			
		||||
            }
 | 
			
		||||
            items(items = entry.second) {
 | 
			
		||||
                Spell(
 | 
			
		||||
                    modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
 | 
			
		||||
                    spell = it,
 | 
			
		||||
                    onClick = onSpell,
 | 
			
		||||
                    onHit = onSpellHit,
 | 
			
		||||
| 
						 | 
				
			
			@ -147,10 +235,15 @@ fun ActionPagePreview() {
 | 
			
		|||
        Surface {
 | 
			
		||||
            ActionsPageContent(
 | 
			
		||||
                modifier = Modifier.fillMaxSize(),
 | 
			
		||||
                header = rememberCharacterHeaderStatePreview(),
 | 
			
		||||
                attacks = rememberAttackListStatePreview(),
 | 
			
		||||
                tokens = rememberTokenListStatePreview(),
 | 
			
		||||
                spells = rememberSpellListStatePreview(),
 | 
			
		||||
                onHitPoint = { },
 | 
			
		||||
                onAttackHit = { },
 | 
			
		||||
                onAttackDamage = { },
 | 
			
		||||
                onToken = { },
 | 
			
		||||
                onSpellLevel = { _, _, _ -> },
 | 
			
		||||
                onSpell = { },
 | 
			
		||||
                onSpellHit = { },
 | 
			
		||||
                onSpellDamage = { },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,118 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.screens.character.pages.actions
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.derivedStateOf
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.lifecycle.SavedStateHandle
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.rplexicon.model.Property
 | 
			
		||||
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.AlterationRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.edit.HpPointDialogUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.factory.CharacterSheetHeaderUioFactory
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.local.sum
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
class HeaderViewModel @Inject constructor(
 | 
			
		||||
    private val headerFactory: CharacterSheetHeaderUioFactory,
 | 
			
		||||
    private val firebaseRepository: FirebaseRepository,
 | 
			
		||||
    characterRepository: CharacterSheetRepository,
 | 
			
		||||
    alterationRepository: AlterationRepository,
 | 
			
		||||
    savedStateHandle: SavedStateHandle,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
    val character = savedStateHandle.characterSheetArgument.name
 | 
			
		||||
 | 
			
		||||
    private val sheetData = mutableStateOf<SheetHeaderData?>(null)
 | 
			
		||||
    private val fireData = mutableStateOf<FireHeaderData?>(null)
 | 
			
		||||
 | 
			
		||||
    val header: State<CharacterSheetHeaderUio?> = derivedStateOf {
 | 
			
		||||
        headerFactory.convert(
 | 
			
		||||
            sheetHeaderData = sheetData.value,
 | 
			
		||||
            fireHeaderData = fireData.value,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val _hitPointDialog = mutableStateOf<HpPointDialogUio?>(null)
 | 
			
		||||
    val hitPointDialog: State<HpPointDialogUio?> get() = _hitPointDialog
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            launch(context = Dispatchers.IO) {
 | 
			
		||||
                characterRepository.data
 | 
			
		||||
                    .combine(alterationRepository.assignedAlterations) { sheets, _ -> sheets }
 | 
			
		||||
                    .collect { sheets ->
 | 
			
		||||
                        val character = sheets.getValue(character)
 | 
			
		||||
                        val alterations = alterationRepository.getActiveAlterationsStatus(
 | 
			
		||||
                            character = character.name,
 | 
			
		||||
                        )
 | 
			
		||||
                        val data = SheetHeaderData(
 | 
			
		||||
                            hpMax = character.hitPoint + alterations[Property.HIT_POINT].sum,
 | 
			
		||||
                            speed = character.speed,
 | 
			
		||||
                            ca = character.armorClass + alterations[Property.ARMOR_CLASS].sum,
 | 
			
		||||
                            dc = character.dC,
 | 
			
		||||
                        )
 | 
			
		||||
                        withContext(Dispatchers.Main) {
 | 
			
		||||
                            sheetData.value = data
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
            launch(context = Dispatchers.IO) {
 | 
			
		||||
                firebaseRepository.getCharacter(character = character).collect { sheets ->
 | 
			
		||||
                    val data = FireHeaderData(
 | 
			
		||||
                        hp = sheets.hitPoint?.value ?: 1,
 | 
			
		||||
                        extra = sheets.hitPoint?.additional ?: 0,
 | 
			
		||||
                    )
 | 
			
		||||
                    withContext(Dispatchers.Main) {
 | 
			
		||||
                        fireData.value = data
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun toggleHitPointDialog() {
 | 
			
		||||
        _hitPointDialog.value = if (_hitPointDialog.value == null) {
 | 
			
		||||
            HpPointDialogUio(
 | 
			
		||||
                value = fireData.value?.hp ?: 10,
 | 
			
		||||
                extra = fireData.value?.extra ?: 0,
 | 
			
		||||
                max = sheetData.value?.hpMax ?: 10,
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun applyHitPointChange(hp: Int, extra: Int) {
 | 
			
		||||
        firebaseRepository.setCharacterHitPoint(
 | 
			
		||||
            character = character,
 | 
			
		||||
            value = hp,
 | 
			
		||||
            extra = extra,
 | 
			
		||||
        )
 | 
			
		||||
        _hitPointDialog.value = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Stable
 | 
			
		||||
    data class SheetHeaderData(
 | 
			
		||||
        val hpMax: Int,
 | 
			
		||||
        val speed: Int,
 | 
			
		||||
        val ca: Int,
 | 
			
		||||
        val dc: Int?,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @Stable
 | 
			
		||||
    data class FireHeaderData(
 | 
			
		||||
        val hp: Int,
 | 
			
		||||
        val extra: Int,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,10 +7,13 @@ import androidx.lifecycle.AndroidViewModel
 | 
			
		|||
import androidx.lifecycle.SavedStateHandle
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.model.AssignedSpell
 | 
			
		||||
import com.pixelized.rplexicon.model.CharacterSheet
 | 
			
		||||
import com.pixelized.rplexicon.model.CharacterSheetFire
 | 
			
		||||
import com.pixelized.rplexicon.model.DiceThrow
 | 
			
		||||
import com.pixelized.rplexicon.model.Property
 | 
			
		||||
import com.pixelized.rplexicon.model.Throw
 | 
			
		||||
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.SpellRepository
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +29,7 @@ import com.pixelized.rplexicon.utilitary.extentions.local.spell
 | 
			
		|||
import com.pixelized.rplexicon.utilitary.extentions.modifier
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
| 
						 | 
				
			
			@ -33,14 +37,16 @@ import kotlin.math.max
 | 
			
		|||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
class SpellsActionViewModel @Inject constructor(
 | 
			
		||||
    application: Application,
 | 
			
		||||
    savedStateHandle: SavedStateHandle,
 | 
			
		||||
    private val characterRepository: CharacterSheetRepository,
 | 
			
		||||
    private val firebaseRepository: FirebaseRepository,
 | 
			
		||||
    private val spellRepository: SpellRepository,
 | 
			
		||||
    application: Application,
 | 
			
		||||
    spellFactory: SpellUioFactory,
 | 
			
		||||
    savedStateHandle: SavedStateHandle,
 | 
			
		||||
) : AndroidViewModel(application) {
 | 
			
		||||
    private val character = savedStateHandle.characterSheetArgument.name
 | 
			
		||||
    private val model: CharacterSheet get() = characterRepository.data.value.getValue(character)
 | 
			
		||||
    private val characterName = savedStateHandle.characterSheetArgument.name
 | 
			
		||||
    private val character: CharacterSheet
 | 
			
		||||
        get() = characterRepository.data.value.getValue(characterName)
 | 
			
		||||
 | 
			
		||||
    private val _spells = mutableStateOf<List<Pair<SpellHeaderUio, List<SpellUio>>>>(emptyList())
 | 
			
		||||
    val spells: State<List<Pair<SpellHeaderUio, List<SpellUio>>>> get() = _spells
 | 
			
		||||
| 
						 | 
				
			
			@ -51,95 +57,107 @@ class SpellsActionViewModel @Inject constructor(
 | 
			
		|||
    init {
 | 
			
		||||
        // TODO rework that part. use factory
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            launch {
 | 
			
		||||
                spellRepository.spells.collect {
 | 
			
		||||
                    _spells.value = withContext(Dispatchers.Default) {
 | 
			
		||||
                        if (model.isWarlock) {
 | 
			
		||||
                            it[character]
 | 
			
		||||
                                ?.sortedBy { it.spell.name }
 | 
			
		||||
                                ?.sortedBy { it.spell.level }
 | 
			
		||||
                                ?.groupBy { it.spell.level == 0 }
 | 
			
		||||
                                ?.map { entry ->
 | 
			
		||||
                                    if (entry.key) {
 | 
			
		||||
                                        SpellHeaderUio(
 | 
			
		||||
                                            level = 0,
 | 
			
		||||
                                            count = null,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        val firstSpellSlot = model.firstSpellSlot()
 | 
			
		||||
                                        SpellHeaderUio(
 | 
			
		||||
                                            level = firstSpellSlot ?: 1,
 | 
			
		||||
                                            count = model.spell(level = firstSpellSlot ?: 1)?.let {
 | 
			
		||||
                                                SpellHeaderUio.Count(
 | 
			
		||||
                                                    it.value,
 | 
			
		||||
                                                    it.max
 | 
			
		||||
                                                )
 | 
			
		||||
                                            }
 | 
			
		||||
                                        )
 | 
			
		||||
                                    } to entry.value.map {
 | 
			
		||||
                                        spellFactory.toUio(
 | 
			
		||||
                                            assignedSpell = it,
 | 
			
		||||
                                            characterSheet = model
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                                ?: emptyList()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            it[character]
 | 
			
		||||
                                ?.sortedBy { it.spell.name }
 | 
			
		||||
                                ?.sortedBy { it.spell.level }
 | 
			
		||||
                                ?.groupBy { it.spell.level }
 | 
			
		||||
                                ?.map { entry ->
 | 
			
		||||
                                    SpellHeaderUio(
 | 
			
		||||
                                        level = entry.key,
 | 
			
		||||
                                        count = model.spell(level = entry.key)?.let {
 | 
			
		||||
                                            SpellHeaderUio.Count(
 | 
			
		||||
                                                it.value,
 | 
			
		||||
                                                it.max
 | 
			
		||||
            launch(Dispatchers.IO) {
 | 
			
		||||
                characterRepository.data
 | 
			
		||||
                    .combine(spellRepository.spells) { sheets, spells ->
 | 
			
		||||
                        Struct(sheets = sheets, spells = spells)
 | 
			
		||||
                    }
 | 
			
		||||
                    .combine(firebaseRepository.getCharacter(character = characterName)) { struct, fire ->
 | 
			
		||||
                        struct.also { it.fire = fire }
 | 
			
		||||
                    }
 | 
			
		||||
                    .collect { data ->
 | 
			
		||||
                        val spells = data.spells[characterName]
 | 
			
		||||
                        val character = data.sheets.getValue(characterName)
 | 
			
		||||
                        _spells.value = withContext(Dispatchers.Default) {
 | 
			
		||||
                            if (character.isWarlock) {
 | 
			
		||||
                                spells
 | 
			
		||||
                                    ?.sortedBy { it.spell.name }
 | 
			
		||||
                                    ?.sortedBy { it.spell.level }
 | 
			
		||||
                                    ?.groupBy { it.spell.level == 0 }
 | 
			
		||||
                                    ?.map { entry ->
 | 
			
		||||
                                        if (entry.key) {
 | 
			
		||||
                                            SpellHeaderUio(
 | 
			
		||||
                                                level = 0,
 | 
			
		||||
                                                count = null,
 | 
			
		||||
                                            )
 | 
			
		||||
                                        },
 | 
			
		||||
                                    ) to entry.value.map {
 | 
			
		||||
                                        spellFactory.toUio(
 | 
			
		||||
                                            assignedSpell = it,
 | 
			
		||||
                                            characterSheet = model
 | 
			
		||||
                                        )
 | 
			
		||||
                                        } else {
 | 
			
		||||
                                            val firstSpellSlot = character.firstSpellSlot()
 | 
			
		||||
                                            SpellHeaderUio(
 | 
			
		||||
                                                level = firstSpellSlot ?: 1,
 | 
			
		||||
                                                count = character.spell(level = firstSpellSlot ?: 1)
 | 
			
		||||
                                                    ?.let { max ->
 | 
			
		||||
                                                        SpellHeaderUio.Count(
 | 
			
		||||
                                                            value = data.fire.spell(
 | 
			
		||||
                                                                level = firstSpellSlot ?: 1
 | 
			
		||||
                                                            ) ?: 0,
 | 
			
		||||
                                                            max = max
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                    }
 | 
			
		||||
                                            )
 | 
			
		||||
                                        } to entry.value.map {
 | 
			
		||||
                                            spellFactory.toUio(
 | 
			
		||||
                                                assignedSpell = it,
 | 
			
		||||
                                                characterSheet = character
 | 
			
		||||
                                            )
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                                ?: emptyList()
 | 
			
		||||
                                    ?: emptyList()
 | 
			
		||||
                            } else {
 | 
			
		||||
                                spells
 | 
			
		||||
                                    ?.sortedBy { it.spell.name }
 | 
			
		||||
                                    ?.sortedBy { it.spell.level }
 | 
			
		||||
                                    ?.groupBy { it.spell.level }
 | 
			
		||||
                                    ?.map { entry ->
 | 
			
		||||
                                        SpellHeaderUio(
 | 
			
		||||
                                            level = entry.key,
 | 
			
		||||
                                            count = character.spell(level = entry.key)?.let { max ->
 | 
			
		||||
                                                SpellHeaderUio.Count(
 | 
			
		||||
                                                    value = data.fire.spell(level = entry.key) ?: 0,
 | 
			
		||||
                                                    max = max
 | 
			
		||||
                                                )
 | 
			
		||||
                                            },
 | 
			
		||||
                                        ) to entry.value.map {
 | 
			
		||||
                                            spellFactory.toUio(
 | 
			
		||||
                                                assignedSpell = it,
 | 
			
		||||
                                                characterSheet = character
 | 
			
		||||
                                            )
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }
 | 
			
		||||
                                    ?: emptyList()
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun shouldDisplaySpellLevelChooser(name: String): Boolean {
 | 
			
		||||
        val assignedSpell = spellRepository.find(
 | 
			
		||||
            character = character,
 | 
			
		||||
            character = characterName,
 | 
			
		||||
            spell = name,
 | 
			
		||||
        )
 | 
			
		||||
        return when (model.isWarlock) {
 | 
			
		||||
        return when (character.isWarlock) {
 | 
			
		||||
            true -> false
 | 
			
		||||
            else -> model.highestSpellLevel() > (assignedSpell?.spell?.level ?: 1)
 | 
			
		||||
            else -> character.highestSpellLevel() > (assignedSpell?.spell?.level ?: 1)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun prepareSpellCast(name: String) {
 | 
			
		||||
        val assignedSpell = spellRepository.find(
 | 
			
		||||
            character = character,
 | 
			
		||||
            character = characterName,
 | 
			
		||||
            spell = name,
 | 
			
		||||
        )
 | 
			
		||||
        if (assignedSpell != null) {
 | 
			
		||||
            val icon = assignedSpell.effect?.faces?.icon ?: R.drawable.ic_d20_24
 | 
			
		||||
            val base = assignedSpell.effect?.toString(character = model, level = 1) ?: ""
 | 
			
		||||
            val base = assignedSpell.effect?.toString(character = character, level = 1) ?: ""
 | 
			
		||||
            _preparedSpellLevel.value = SpellChooserUio(
 | 
			
		||||
                name = name,
 | 
			
		||||
                spells = List(
 | 
			
		||||
                    size = max(0, model.highestSpellLevel() + 1 - assignedSpell.spell.level)
 | 
			
		||||
                    size = max(0, character.highestSpellLevel() + 1 - assignedSpell.spell.level)
 | 
			
		||||
                ) { index ->
 | 
			
		||||
                    val level = assignedSpell.spell.level + index
 | 
			
		||||
                    val remaining = model.spell(level)?.value
 | 
			
		||||
                    val max = model.spell(level)?.max
 | 
			
		||||
                    val remaining = character.spell(level)
 | 
			
		||||
                    val max = character.spell(level)
 | 
			
		||||
 | 
			
		||||
                    SpellLevelUio(
 | 
			
		||||
                        spell = assignedSpell.spell.name,
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +166,7 @@ class SpellsActionViewModel @Inject constructor(
 | 
			
		|||
                        max = max,
 | 
			
		||||
                        icon = icon,
 | 
			
		||||
                        value = base + (assignedSpell.level
 | 
			
		||||
                            ?.toString(character = model, level = index)
 | 
			
		||||
                            ?.toString(character = character, level = index)
 | 
			
		||||
                            ?.let { " + $it" }
 | 
			
		||||
                            ?: "")
 | 
			
		||||
                    )
 | 
			
		||||
| 
						 | 
				
			
			@ -158,26 +176,26 @@ class SpellsActionViewModel @Inject constructor(
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    fun onCastSpell(id: String): DiceThrow {
 | 
			
		||||
        val spell = spellRepository.find(character = character, spell = id)
 | 
			
		||||
        val spell = spellRepository.find(character = characterName, spell = id)
 | 
			
		||||
        return onCastSpell(
 | 
			
		||||
            id = id,
 | 
			
		||||
            level = when (model.isWarlock) {
 | 
			
		||||
                true -> model.firstSpellSlot() ?: 1
 | 
			
		||||
            level = when (character.isWarlock) {
 | 
			
		||||
                true -> character.firstSpellSlot() ?: 1
 | 
			
		||||
                else -> spell?.spell?.level ?: 1
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onCastSpell(id: String, level: Int): DiceThrow {
 | 
			
		||||
        return DiceThrow.SpellEffect(character = character, spell = id, level = level)
 | 
			
		||||
        return DiceThrow.SpellEffect(character = characterName, spell = id, level = level)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onSpellHitRoll(id: String): DiceThrow {
 | 
			
		||||
        return DiceThrow.SpellAttack(character = character, spell = id)
 | 
			
		||||
        return DiceThrow.SpellAttack(character = characterName, spell = id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onSpellDamageRoll(id: String): DiceThrow {
 | 
			
		||||
        return DiceThrow.SpellDamage(character = character, spell = id)
 | 
			
		||||
        return DiceThrow.SpellDamage(character = characterName, spell = id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -216,4 +234,11 @@ class SpellsActionViewModel @Inject constructor(
 | 
			
		|||
            else -> 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class Struct(
 | 
			
		||||
        val sheets: Map<String, CharacterSheet>,
 | 
			
		||||
        val spells: Map<String, List<AssignedSpell>>,
 | 
			
		||||
    ) {
 | 
			
		||||
        lateinit var fire: CharacterSheetFire
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,129 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.screens.character.pages.actions
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.lifecycle.AndroidViewModel
 | 
			
		||||
import androidx.lifecycle.SavedStateHandle
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.model.CharacterSheetFire
 | 
			
		||||
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.AlterationRepository
 | 
			
		||||
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.edit.CounterEditDialogUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
 | 
			
		||||
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItemUio
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.context
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@HiltViewModel
 | 
			
		||||
class TokenViewModel @Inject constructor(
 | 
			
		||||
    private val characterRepository: CharacterSheetRepository,
 | 
			
		||||
    private val firebaseRepository: FirebaseRepository,
 | 
			
		||||
    private val alterationRepository: AlterationRepository,
 | 
			
		||||
    application: Application,
 | 
			
		||||
    savedStateHandle: SavedStateHandle,
 | 
			
		||||
) : AndroidViewModel(application) {
 | 
			
		||||
    private val character = savedStateHandle.characterSheetArgument.name
 | 
			
		||||
 | 
			
		||||
    private val _dialog = mutableStateOf<CounterEditDialogUio?>(null)
 | 
			
		||||
    val dialog: State<CounterEditDialogUio?> get() = _dialog
 | 
			
		||||
 | 
			
		||||
    private val _counters = mutableStateOf<List<TokenItemUio>>(emptyList())
 | 
			
		||||
    val tokens: State<List<TokenItemUio>> get() = _counters
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            launch(Dispatchers.IO) {
 | 
			
		||||
                characterRepository.data
 | 
			
		||||
                    .combine(firebaseRepository.getCharacter(character = character)) { sheets, fire ->
 | 
			
		||||
                        sheets.getValue(character) to fire
 | 
			
		||||
                    }
 | 
			
		||||
                    .collect { data ->
 | 
			
		||||
                        val (character, fire) = data
 | 
			
		||||
                        val counters = mutableListOf<TokenItemUio>()
 | 
			
		||||
                        character.rage?.let {
 | 
			
		||||
                            counters.add(
 | 
			
		||||
                                TokenItemUio(
 | 
			
		||||
                                    id = CharacterSheetFire.RAGE,
 | 
			
		||||
                                    icon = R.drawable.ic_fist_24,
 | 
			
		||||
                                    label = R.string.token_label_rage,
 | 
			
		||||
                                    value = fire.rage ?: 0,
 | 
			
		||||
                                    max = it,
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        character.relentlessEndurance?.let {
 | 
			
		||||
                            counters.add(
 | 
			
		||||
                                TokenItemUio(
 | 
			
		||||
                                    id = CharacterSheetFire.RELENTLESS_ENDURANCE,
 | 
			
		||||
                                    icon = R.drawable.ic_burning_passion_24,
 | 
			
		||||
                                    label = R.string.token_label_relentless_endurance,
 | 
			
		||||
                                    value = fire.relentlessEndurance ?: 0,
 | 
			
		||||
                                    max = it,
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        character.bardicInspiration?.let {
 | 
			
		||||
                            counters.add(
 | 
			
		||||
                                TokenItemUio(
 | 
			
		||||
                                    id = CharacterSheetFire.BARDIC_INSPIRATION,
 | 
			
		||||
                                    icon = R.drawable.ic_lyre_24,
 | 
			
		||||
                                    label = R.string.token_label_bardic_inspiration,
 | 
			
		||||
                                    value = fire.bardicInspiration ?: 0,
 | 
			
		||||
                                    max = it,
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        character.divineConduit?.let {
 | 
			
		||||
                            counters.add(
 | 
			
		||||
                                TokenItemUio(
 | 
			
		||||
                                    id = CharacterSheetFire.DIVINE_CONDUIT,
 | 
			
		||||
                                    icon = R.drawable.ic_embrassed_energy_24,
 | 
			
		||||
                                    label = R.string.token_label_divine_conduit,
 | 
			
		||||
                                    value = fire.divineConduit ?: 0,
 | 
			
		||||
                                    max = it,
 | 
			
		||||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        withContext(Dispatchers.Main) {
 | 
			
		||||
                            _counters.value = counters
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun showTokenEditDialog(dialog: TokenItemUio) {
 | 
			
		||||
        _dialog.value = CounterEditDialogUio(
 | 
			
		||||
            id = dialog.id,
 | 
			
		||||
            label = context.getString(dialog.label),
 | 
			
		||||
            value = dialog.value,
 | 
			
		||||
            max = dialog.max,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun showSpellTokenEditDialog(level: Int, value: Int, max: Int) {
 | 
			
		||||
        _dialog.value = CounterEditDialogUio(
 | 
			
		||||
            id = CharacterSheetFire.SPELL_SLOT_LEVEL_X + level,
 | 
			
		||||
            label = context.getString(R.string.spell_level_chooser_label, "$level"),
 | 
			
		||||
            value = value,
 | 
			
		||||
            max = max,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun hideCounterEditDialog() {
 | 
			
		||||
        _dialog.value = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun applyCounterValue(id: String, value: Int) {
 | 
			
		||||
        firebaseRepository.setToken(character = character, token = id, value = value)
 | 
			
		||||
        _dialog.value = null
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -34,12 +34,12 @@ fun AlterationPage(
 | 
			
		|||
 | 
			
		||||
    AlterationPageContent(
 | 
			
		||||
        alterations = viewModel.alterations,
 | 
			
		||||
        onInfo = {
 | 
			
		||||
        onAlterationInfo = {
 | 
			
		||||
            viewModel.showAlterationDetail(id = it)
 | 
			
		||||
        },
 | 
			
		||||
        onClick = {
 | 
			
		||||
        onAlterationClick = {
 | 
			
		||||
            scope.launch {
 | 
			
		||||
                viewModel.toggle(alteration = it)
 | 
			
		||||
                viewModel.toggleAlteration(alteration = it)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			@ -54,8 +54,8 @@ fun AlterationPage(
 | 
			
		|||
fun AlterationPageContent(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    alterations: State<List<RollAlterationUio>>,
 | 
			
		||||
    onInfo: (String) -> Unit,
 | 
			
		||||
    onClick: (String) -> Unit,
 | 
			
		||||
    onAlterationInfo: (String) -> Unit,
 | 
			
		||||
    onAlterationClick: (String) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +65,8 @@ fun AlterationPageContent(
 | 
			
		|||
            RollAlteration(
 | 
			
		||||
                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                alteration = it,
 | 
			
		||||
                onInfo = onInfo,
 | 
			
		||||
                onClick = onClick,
 | 
			
		||||
                onInfo = onAlterationInfo,
 | 
			
		||||
                onClick = onAlterationClick,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -98,8 +98,8 @@ fun AlterationPagePreview() {
 | 
			
		|||
        Surface {
 | 
			
		||||
            AlterationPageContent(
 | 
			
		||||
                alterations = rememberRollAlterations(),
 | 
			
		||||
                onInfo = { },
 | 
			
		||||
                onClick = { },
 | 
			
		||||
                onAlterationInfo = { },
 | 
			
		||||
                onAlterationClick = { },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,156 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
 | 
			
		||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
 | 
			
		||||
import androidx.annotation.DrawableRes
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.heightIn
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Surface
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class TokenItemUio(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    @DrawableRes val icon: Int,
 | 
			
		||||
    @StringRes val label: Int,
 | 
			
		||||
    val value: Int,
 | 
			
		||||
    val max: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun TokenItem(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    padding: PaddingValues = PaddingValues(horizontal = 16.dp),
 | 
			
		||||
    counter: TokenItemUio,
 | 
			
		||||
    onClick: (TokenItemUio) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .clickable { onClick(counter) }
 | 
			
		||||
            .heightIn(min = 52.dp)
 | 
			
		||||
            .padding(paddingValues = padding)
 | 
			
		||||
            .then(other = modifier),
 | 
			
		||||
        contentAlignment = Alignment.Center,
 | 
			
		||||
    ) {
 | 
			
		||||
        Row(
 | 
			
		||||
            modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
            verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
            horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
 | 
			
		||||
        ) {
 | 
			
		||||
            Icon(
 | 
			
		||||
                painter = painterResource(id = counter.icon),
 | 
			
		||||
                contentDescription = null,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            Text(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .alignByBaseline()
 | 
			
		||||
                    .weight(weight = 1f),
 | 
			
		||||
                style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
                fontWeight = FontWeight.Bold,
 | 
			
		||||
                overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                maxLines = 1,
 | 
			
		||||
                text = stringResource(id = counter.label),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
 | 
			
		||||
            ) {
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.titleLarge,
 | 
			
		||||
                    fontWeight = FontWeight.Bold,
 | 
			
		||||
                    color = MaterialTheme.colorScheme.primary,
 | 
			
		||||
                    text = "${counter.value}",
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.bodySmall,
 | 
			
		||||
                    fontWeight = FontWeight.Light,
 | 
			
		||||
                    text = "/"
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                    style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
                    fontWeight = FontWeight.Light,
 | 
			
		||||
                    text = "${counter.max}",
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@Stable
 | 
			
		||||
fun rememberTokenListStatePreview(): State<List<TokenItemUio>> = remember {
 | 
			
		||||
    mutableStateOf(
 | 
			
		||||
        listOf(
 | 
			
		||||
            TokenItemUio(
 | 
			
		||||
                id = "1",
 | 
			
		||||
                icon = R.drawable.ic_fist_24,
 | 
			
		||||
                label = R.string.token_label_rage,
 | 
			
		||||
                value = 2,
 | 
			
		||||
                max = 2,
 | 
			
		||||
            ),
 | 
			
		||||
            TokenItemUio(
 | 
			
		||||
                id = "2",
 | 
			
		||||
                icon = R.drawable.ic_embrassed_energy_24,
 | 
			
		||||
                label = R.string.token_label_divine_conduit,
 | 
			
		||||
                value = 2,
 | 
			
		||||
                max = 4,
 | 
			
		||||
            ),
 | 
			
		||||
            TokenItemUio(
 | 
			
		||||
                id = "3",
 | 
			
		||||
                icon = R.drawable.ic_lyre_24,
 | 
			
		||||
                label = R.string.token_label_bardic_inspiration,
 | 
			
		||||
                value = 2,
 | 
			
		||||
                max = 3,
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
 | 
			
		||||
@Preview(uiMode = UI_MODE_NIGHT_YES)
 | 
			
		||||
private fun CounterItemPreview() {
 | 
			
		||||
    LexiconTheme {
 | 
			
		||||
        Surface {
 | 
			
		||||
            TokenItem(
 | 
			
		||||
                counter = TokenItemUio(
 | 
			
		||||
                    id = "0",
 | 
			
		||||
                    icon = R.drawable.ic_burning_passion_24,
 | 
			
		||||
                    label = R.string.token_label_relentless_endurance,
 | 
			
		||||
                    value = 2,
 | 
			
		||||
                    max = 2,
 | 
			
		||||
                ),
 | 
			
		||||
                onClick = { },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,18 +13,6 @@ fun List<Alteration>.toStatus(): Map<Property, List<Alteration.Status>> {
 | 
			
		|||
    return status
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun List<Alteration>.toActiveStatus(): Map<Property, List<Alteration.Status>> {
 | 
			
		||||
    val status = hashMapOf<Property, MutableList<Alteration.Status>>()
 | 
			
		||||
    forEach { alteration ->
 | 
			
		||||
        if (alteration.active) {
 | 
			
		||||
            alteration.status.forEach {
 | 
			
		||||
                status.getOrPut(it.key) { mutableListOf() }.add(it.value)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return status
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val List<Alteration.Status>?.sum: Int
 | 
			
		||||
    get() = this?.sumOf { alt -> alt.bonus.sumOf { it.value } } ?: 0
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								app/src/main/res/drawable/ic_bloody_stash_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/res/drawable/ic_bloody_stash_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
<vector android:height="24dp" android:viewportHeight="512"
 | 
			
		||||
    android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <path android:fillColor="#000000" android:pathData="M244.19,21.97C347.89,119.18 428.1,216.27 494.72,304.06L494.72,191.66c-7.06,-6.39 -14.15,-12.68 -21.34,-18.84 2.99,-3.46 4.81,-7.97 4.81,-12.91 0,-10.93 -8.85,-19.78 -19.78,-19.78 -6.28,0 -11.88,2.96 -15.5,7.53 -43.25,-34.45 -88.29,-64.96 -134,-91.31 1.01,-2.03 1.59,-4.27 1.59,-6.69 0,-8.35 -6.77,-15.13 -15.13,-15.13 -5.42,0 -10.14,2.85 -12.81,7.13 -12.76,-6.88 -25.57,-13.45 -38.38,-19.69zM104.63,40.09c5.11,5.18 10.18,10.36 15.22,15.59 -4.22,1.17 -7.38,4.29 -8.35,9 -1.81,8.84 4.73,19.92 14.63,24.72 3.64,1.77 7.24,2.42 10.41,2.12 -2.54,4.11 -4,8.97 -4,14.16 0,14.89 12.05,26.94 26.94,26.94 8.68,0 16.41,-4.1 21.34,-10.47 88.69,101.2 165.6,208.4 235.16,306.72 -7.4,6.05 -12.13,15.23 -12.13,25.53 0,18.21 14.76,32.97 32.97,32.97 6.27,0 12.13,-1.77 17.12,-4.81 3.09,4.35 6.19,8.76 9.25,13.06h31.53L494.72,381.28c-41.36,-69.22 -154.77,-193.34 -184.53,-213.31 24.14,33.4 45.1,64.34 64.81,94.03 -87.49,-95.75 -183.44,-179.59 -270.38,-221.91zM76.4,55.34c-1.87,0.09 -3.74,0.55 -5.53,1.41 -7.18,3.43 -10.21,12.04 -6.78,19.22 3.43,7.18 12.04,10.21 19.22,6.78 7.18,-3.43 10.21,-12.04 6.78,-19.22 -2.57,-5.38 -8.08,-8.44 -13.69,-8.19zM432.15,75.72c-1.45,0.05 -2.86,0.36 -4.16,0.97 -6.92,3.26 -8.27,13.63 -3,23.16 5.26,9.53 15.14,14.63 22.06,11.38 6.92,-3.26 8.26,-13.63 3,-23.16 -4.28,-7.74 -11.6,-12.56 -17.91,-12.34zM281.03,110.97c-1.98,0.12 -3.85,0.72 -5.44,1.78 -6.37,4.24 -6.17,14.69 0.44,23.34 6.61,8.65 17.1,12.24 23.47,8 6.37,-4.24 6.17,-14.69 -0.44,-23.34 -4.95,-6.49 -12.08,-10.15 -18.03,-9.78zM25.19,149.28c91.02,100.04 158.7,190.4 212,271.6 -8.48,-1.36 -18.44,4.95 -20.25,13.59 -6.22,29.78 12.9,53.91 47.59,61.15L390.19,495.62c-97.97,-139.74 -234.78,-282.95 -365,-346.34zM55.22,251.13c-3.81,0.08 -7.2,1.33 -9.41,3.84 -5.05,5.74 -2.15,15.78 6.47,22.44 8.62,6.65 19.7,7.4 24.75,1.66 5.05,-5.74 2.18,-15.79 -6.44,-22.44 -4.85,-3.74 -10.48,-5.6 -15.38,-5.5zM102.41,289.25c-14.89,0 -26.94,12.05 -26.94,26.94 0,14.89 12.05,26.97 26.94,26.97 14.89,0 26.97,-12.08 26.97,-26.97 0,-14.89 -12.08,-26.94 -26.97,-26.94zM182.13,392.72c-10.93,0 -19.78,8.85 -19.78,19.78 0,10.93 8.85,19.78 19.78,19.78 10.93,0 19.78,-8.85 19.78,-19.78 0,-10.93 -8.85,-19.78 -19.78,-19.78z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_burning_passion_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_burning_passion_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="512"
 | 
			
		||||
    android:viewportHeight="512">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#000000"
 | 
			
		||||
        android:pathData="M19.84,18.75v2.72c64.27,50.96 95.31,115.85 89.44,179.25 -10.6,-55 -41.76,-104.8 -89.44,-138.91v23.53c55.24,45.18 82.41,114.31 72.97,185.25 -0.4,2.33 -0.76,4.66 -1.06,7 -0.09,0.51 -0.16,1.02 -0.25,1.53h0.06c-6.55,53.8 11.2,108.57 49.59,156.03 6.41,11.07 13.98,21.8 22.69,32.13C95.25,406.66 59.08,335.53 53.22,262.41c-11.11,83 15.11,163.21 90.69,230.22L188.5,492.63c0.03,0.03 0.06,0.06 0.09,0.09h130.47c0.03,-0.03 0.06,-0.06 0.09,-0.09h43.63c75.58,-67.01 101.8,-147.22 90.69,-230.22 -5.75,71.77 -40.7,141.62 -106.85,201.5 9.56,-11.75 17.68,-24.02 24.28,-36.69 34.1,-45.58 49.6,-97.28 43.41,-148.1 -0,-0.02 0,-0.04 0,-0.06 -0.41,-3.31 -0.91,-6.61 -1.5,-9.91 -9.36,-74.25 21.31,-146.35 82.31,-190.88L495.13,55.63c-52.75,34.07 -87.21,86.5 -98.53,144.84 -5.85,-64.21 26.1,-129.92 92.12,-181.13l-0.47,-0.59h-28.06c-72.05,64.34 -99.85,149.67 -72.5,228.06 2.89,8.29 5.11,16.68 6.66,25.09 0,0.04 0.03,0.08 0.03,0.13 0.44,3.43 0.93,6.88 1.53,10.31h0.03c2.3,19.37 1.12,38.89 -3.4,58.16 -0.04,-28.38 -6.78,-57.15 -20.44,-85.06 -40.06,-81.86 -20.77,-171.43 52.41,-236.69h-31.03c-50.15,46.62 -66.32,91.56 -57.44,151.09 -21.49,-59.17 -19.42,-103.58 20.69,-151.09L152,18.75c40.1,47.51 42.18,91.93 20.69,151.09 8.89,-59.53 -7.27,-104.47 -57.41,-151.09L83.16,18.75c73.17,65.26 92.46,154.82 52.41,236.69 -14.9,30.45 -21.52,61.92 -20.25,92.78 -6.15,-21.75 -8.02,-43.91 -5.41,-65.87 0.6,-3.44 1.12,-6.88 1.56,-10.31 1.55,-8.46 3.78,-16.88 6.69,-25.22 27.35,-78.39 -0.45,-163.72 -72.5,-228.06L19.84,18.75zM254.09,38.44c16.4,0 27.02,6.18 34.72,16.59 7.69,10.41 11.97,25.73 11.97,43 0,18.66 -6.89,38.56 -15.97,49.5l-10.13,12.22 15.59,2.94c12.52,2.35 21.72,8.77 29.44,19 7.72,10.23 13.57,24.36 17.69,40.69 7.52,29.84 9.14,66.52 9.38,99.34h-23.31l-0.81,-70.5 -18.69,0.22 0.97,86.44 -7.75,111.63c47.06,-43.67 71.99,-94.3 76.16,-146.31 8.21,61.34 -11.15,120.61 -67,170.13L295.53,473.31v0.22h-32.94L262.59,333.81h-18.69v139.72L212.62,473.53v-0.22h-9.65c-55.85,-49.52 -75.24,-108.79 -67.03,-170.13 4.13,51.56 28.66,101.78 74.94,145.19L203.78,345.72l2.6,-94.13 -18.69,-0.53 -1.94,70.65h-24.38c0.24,-32.83 1.88,-69.5 9.41,-99.35 4.12,-16.33 9.97,-30.46 17.69,-40.69 7.72,-10.23 16.92,-16.65 29.44,-19l15.59,-2.94 -10.13,-12.22c-9.08,-10.94 -15.97,-30.83 -15.97,-49.5 0,-17.27 4.28,-32.59 11.97,-43 7.69,-10.41 18.32,-16.59 34.72,-16.59z" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_embrassed_energy_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_embrassed_energy_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="512"
 | 
			
		||||
    android:viewportHeight="512">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#000000"
 | 
			
		||||
        android:pathData="M179.81,20.72v81.25L135.78,75.62l17.56,46.94 -115.66,-20.94 84.72,49.91H20v27.34l110.47,14.88 96.59,-29.19c-11.3,-11.87 -18.59,-30.74 -18.59,-52 0,-35.93 20.87,-65.06 46.62,-65.06 25.75,0 46.63,29.14 46.63,65.06 0,20.85 -7.04,39.38 -17.97,51.28l99.03,29.91 112.5,-15.16V151.53H394.19l84.72,-49.9 -120.44,21.78 17.87,-47.72 -48.66,29.13V20.72H179.81zM495.28,223.34l-112.5,22.44 -55.4,-13.12 -28.03,118.31 16.59,145h51.69L329.25,351.22l46.53,27.84 -21.31,-56.94 124.44,22.5 -91.13,-53.69h107.5v-67.59zM20,223.75v67.19h108.81l-91.13,53.69L157.31,322.97 136.35,379l38.47,-23 -28.59,139.97h48.15L207.28,351.56 185.6,232.72l-55.13,13.06L20,223.75z" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_fist_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_fist_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="512"
 | 
			
		||||
    android:viewportHeight="512">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#000000"
 | 
			
		||||
        android:pathData="M227.23,21.78c-1.85,0 -3.7,0.05 -5.57,0.16 -15.31,0.88 -30.76,5.3 -39.49,10.86l-0.01,73.15c2.88,-0.09 5.78,-0.15 8.68,-0.14 23.38,0.04 47.1,3.29 68.47,9.51l0.01,-87.51c-7.03,-3.52 -19.18,-6.03 -32.09,-6.03zM307.97,30.94c-11.93,0.15 -23.08,2.36 -29.97,5.6l-0.01,77.6v7.66c38.49,15.67 64.81,42.48 58.74,78.76l-0.96,5.73 -5.56,1.67c-17.45,5.25 -34.87,9.7 -52.22,13.34L277.98,246.53c25.56,-0.7 51.33,-2.69 77.14,-6.1l0.02,-197.93c-8.28,-5.56 -23.51,-10.24 -38.84,-11.33 -2.79,-0.2 -5.58,-0.27 -8.34,-0.24zM143.22,46.29c-1.18,-0.01 -2.37,-0.01 -3.59,0.02 -4.18,0.1 -8.53,0.47 -12.9,1.15 -15.67,2.45 -31.48,8.56 -40.41,15.4l-0.01,72.96c18.81,-15.81 46.7,-25.14 77.15,-28.54l0.01,-57.97c-4.82,-1.75 -12.02,-2.92 -20.25,-3.02zM401.62,49.75c-10.8,0.12 -20.72,1.93 -27.04,4.66l-0.02,183.18c25.07,-4.02 50.16,-9.41 75.12,-16.36l1.99,-158.45c-8.35,-5.9 -23.65,-11.02 -39.05,-12.55 -3.7,-0.37 -7.4,-0.52 -11,-0.48zM178.84,123.96c-53.72,0.7 -101.41,20.36 -97.89,66.6 15.84,-3.92 30.84,-5.89 44.94,-6.1 34.84,-0.51 64.21,9.7 87.32,27.61 34.61,-3.11 69.85,-10 105.41,-20.31 0.14,-41.29 -74.1,-68.66 -139.78,-67.8zM129.97,202.61c-1.3,-0 -2.6,0.01 -3.92,0.05 -17.26,0.44 -36.45,4.03 -57.57,11.04 5.79,53.81 26.33,106.41 58.5,143.35 6.23,7.15 12.86,13.71 19.88,19.61 29.3,9.28 69.26,12.92 110.53,12.14 3.78,-55.81 -8.72,-108.36 -36.19,-142.74 -21.26,-26.61 -51.06,-43.39 -91.23,-43.44zM259.29,224.89c-9.36,1.64 -18.69,3.02 -28,4.15 1.54,1.74 3.04,3.52 4.5,5.35 3.15,3.94 6.09,8.06 8.87,12.33 9.92,0.14 19.87,0.13 29.86,-0.11L259.29,246.61v-21.72zM451.11,240.23c-65.41,17.83 -131.46,25.41 -195.85,25.32 17,35.14 23.83,78.09 21.01,122.6 42.48,-2.08 85.03,-8.23 118.19,-15.98 26.69,-32.78 47.37,-77.12 56.65,-131.93zM400.51,389.9c-38.33,9.15 -87.95,16.06 -136.87,17.45 -47.67,1.36 -94.34,-2.23 -129.45,-15.26l-0.01,78.93c27.19,12.57 76.41,20.2 127.32,20.3 51.22,0.09 104.21,-7.17 139,-20.77l0.01,-80.65z" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										4
									
								
								app/src/main/res/drawable/ic_layered_armor_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/res/drawable/ic_layered_armor_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
<vector android:height="24dp" android:viewportHeight="512"
 | 
			
		||||
    android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <path android:fillColor="#000000" android:pathData="M129.66,21.19L37.94,79.78c3.54,26.81 8.91,53.55 16.13,80.13L240.72,39.59l-19.28,-12.5c-31.28,-0.88 -62.2,-2.84 -91.78,-5.91zM383.13,21.81c-40.51,3.97 -83.5,5.94 -126.47,5.84l204.63,132.72c7.11,-25.89 12.49,-51.92 16.09,-78.03l-94.25,-60.53zM257.94,50.75L59.47,178.66c8.02,26.32 17.86,52.46 29.53,78.31l243.25,-158 -74.31,-48.22zM349.41,110.09l-74.56,48.44 151.28,98.78c11.71,-25.8 21.59,-51.91 29.69,-78.19l-106.41,-69.03zM257.72,169.66L97,274.06c12.2,25.17 26.14,50.06 41.84,74.56l196.09,-128.53 -77.22,-50.44zM352,231.22l-77.53,50.84 101.4,67.19c15.82,-24.6 29.9,-49.58 42.22,-74.88L352,231.22zM257.47,293.19l-108.35,71.03c13.56,20.06 28.33,39.85 44.28,59.31l132.03,-85.28 -67.97,-45.06zM342.44,349.5L274.5,393.41l47.03,30.38c15.85,-19.34 30.51,-38.99 44.03,-58.94L342.44,349.5zM257.47,404.38L205.5,437.97c16.23,18.93 33.61,37.54 52.16,55.78 18.39,-18.15 35.64,-36.68 51.78,-55.53l-52.09,-33.63 0.13,-0.22z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_lyre_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_lyre_24.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="512"
 | 
			
		||||
    android:viewportHeight="512">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#000000"
 | 
			
		||||
        android:pathData="M108.66,35.06c-15.05,0.14 -33.41,5.38 -46.97,15.81 -10.75,8.28 -18.78,19.27 -21.19,34.44 0.21,-0.13 0.41,-0.25 0.63,-0.38 -0.84,2.82 -1.31,5.79 -1.31,8.88 0,17.09 13.85,30.94 30.94,30.94 14.29,0 26.32,-9.7 29.88,-22.88 0.03,-0.12 0.07,-0.23 0.09,-0.34 0.48,-2.08 0.77,-4.04 0.9,-5.84 0.03,-0.33 0.02,-0.65 0.03,-0.97 0,-0.13 0.03,-0.25 0.03,-0.38 0.01,-0.18 -0,-0.35 0,-0.53 0,-1.53 -0.1,-3.04 -0.31,-4.5 -1.37,-8.02 -6.78,-12.16 -12.59,-13.72 -8.53,-2.29 -19.06,0.64 -23.75,18.16l-0.47,-0.13C62.04,74.12 72.21,63.88 83.94,60.78c2.48,-0.65 5.05,-1 7.66,-0.97 9.07,0.13 18.44,4.88 24.56,17.13 0.09,0.17 0.17,0.35 0.25,0.53 5.21,15.23 2.11,43.32 -3.34,57.63 -7.29,18.75 -22.38,40.5 -47.69,65.5C6.99,258.25 4,329.82 39.97,388.81 75.94,447.8 152.13,493.56 254.44,493.56c102.31,0 178.47,-45.76 214.44,-104.75 35.88,-58.85 32.98,-130.23 -25,-187.81l-0.41,-0.41h-0.03c-25.31,-25 -40.37,-46.75 -47.66,-65.5 -5.23,-16.45 -9.09,-42.99 -2.65,-57.63 0.06,-0.13 0.13,-0.25 0.19,-0.38 0.03,-0.05 0.04,-0.11 0.06,-0.16 6.12,-12.25 15.49,-17 24.56,-17.13 2.6,-0.04 5.18,0.31 7.66,0.97 11.72,3.1 21.87,13.34 19.34,32.84l-0.44,0.13c-4.69,-17.52 -15.22,-20.45 -23.75,-18.16 -4.41,1.18 -8.6,3.85 -10.97,8.56 -0.01,0.04 -0.02,0.09 -0.03,0.13 -0.98,3.01 -1.5,6.2 -1.5,9.53 0,17.09 13.85,30.94 30.94,30.94 17.09,0 30.94,-13.85 30.94,-30.94 0,-4.36 -0.91,-8.49 -2.53,-12.25 -3.06,-13.24 -10.6,-23.11 -20.44,-30.69 -14.46,-11.13 -34.39,-16.36 -49.94,-15.78 -13.38,0.5 -24.85,4.11 -33.22,10.53 -3.41,2.62 -6.38,5.7 -8.84,9.38 -69.46,35.51 -138.89,38.75 -208.34,-7.75 -0.64,-0.56 -1.29,-1.11 -1.97,-1.63 -8.37,-6.42 -19.84,-10.03 -33.22,-10.53 -0.97,-0.04 -1.96,-0.04 -2.97,-0.03zM161.44,88.19c6.34,2.65 12.67,4.99 19,7.03v313.06c-30.73,-8.26 -57.89,-22 -77.37,-41.31 -17.1,-16.94 -28.08,-38.63 -28.91,-63.6 -0.83,-24.97 8.27,-52.7 28.97,-82.63 41.32,-59.75 57.16,-103.6 58.31,-132.56zM347.47,89.59c1.6,28.97 17.59,72.37 58.25,131.16 20.69,29.92 29.8,57.66 28.97,82.63 -0.83,24.97 -11.81,46.65 -28.91,63.59 -19.02,18.85 -45.37,32.4 -75.22,40.72L330.56,94.84c5.64,-1.61 11.27,-3.35 16.91,-5.25zM311.87,99.59v312.53c-8.21,1.63 -16.61,2.88 -25.13,3.78L286.75,104.09c8.38,-1.12 16.75,-2.63 25.13,-4.5zM199.12,100.37c8.45,1.97 16.89,3.44 25.34,4.44v311.34c-8.59,-0.84 -17.06,-2.05 -25.34,-3.63L199.12,100.38zM268.06,105.94v311.38c-4.54,0.2 -9.09,0.31 -13.66,0.31 -3.76,0 -7.51,-0.05 -11.25,-0.19L243.16,106.28c8.29,0.31 16.61,0.18 24.91,-0.34z" />
 | 
			
		||||
</vector>
 | 
			
		||||
| 
						 | 
				
			
			@ -154,4 +154,10 @@
 | 
			
		|||
 | 
			
		||||
    <string name="alteration_source">Source : %1$s</string>
 | 
			
		||||
    <string name="alteration_target">Cible : %1$s</string>
 | 
			
		||||
 | 
			
		||||
    <string name="token_label_title">Capacité</string>
 | 
			
		||||
    <string name="token_label_rage">Rage</string>
 | 
			
		||||
    <string name="token_label_relentless_endurance">Endurance Implacable</string>
 | 
			
		||||
    <string name="token_label_bardic_inspiration">Inspiration Bardique</string>
 | 
			
		||||
    <string name="token_label_divine_conduit">Conduit Divin</string>
 | 
			
		||||
</resources>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
    <string name="google_spreadsheet_api_key">AIzaSyBmagVOEyB68tTJ5QMFMzMQZIHG_4XVCOo</string>
 | 
			
		||||
    <string name="google_sign_in_id" translatable="false">62913404482-ergqkjiuvint49q8lm555j21vvb6af7s.apps.googleusercontent.com</string>
 | 
			
		||||
    <string name="google_spreadsheet_api_key" translatable="false">AIzaSyBmagVOEyB68tTJ5QMFMzMQZIHG_4XVCOo</string>
 | 
			
		||||
    <string name="firebase_realtime_database" translatable="false">https://rp-lexicon-default-rtdb.europe-west1.firebasedatabase.app/</string>
 | 
			
		||||
</resources>
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +75,7 @@
 | 
			
		|||
    <string name="map_label">Coordinates</string>
 | 
			
		||||
 | 
			
		||||
    <string name="character_sheet_title">Character sheet</string>
 | 
			
		||||
    <string name="character_sheet_title_hp">HP</string>
 | 
			
		||||
    <string name="character_sheet_title_hp">Hit Point</string>
 | 
			
		||||
    <string name="character_sheet_title_ca">CA</string>
 | 
			
		||||
    <string name="character_sheet_title_dc">DC</string>
 | 
			
		||||
    <string name="character_sheet_title_speed">Speed</string>
 | 
			
		||||
| 
						 | 
				
			
			@ -154,4 +154,10 @@
 | 
			
		|||
 | 
			
		||||
    <string name="alteration_source">Source: %1$s</string>
 | 
			
		||||
    <string name="alteration_target">Target: %1$s</string>
 | 
			
		||||
 | 
			
		||||
    <string name="token_label_title">Skill</string>
 | 
			
		||||
    <string name="token_label_rage">Rage</string>
 | 
			
		||||
    <string name="token_label_relentless_endurance">Relentless Endurance</string>
 | 
			
		||||
    <string name="token_label_bardic_inspiration">Bardic Inspiration</string>
 | 
			
		||||
    <string name="token_label_divine_conduit">Divine Conduit</string>
 | 
			
		||||
</resources>
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue