Change the roll bahavior (state to shared) after levelup.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2025-12-09 14:46:50 +01:00
parent 03dbd7aad6
commit db98fbede7
5 changed files with 84 additions and 52 deletions

View file

@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa.repository.resources
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap
import com.pixelized.desktop.lwa.ui.composable.image.ImagerModelConverter
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.readRawBytes import io.ktor.client.statement.readRawBytes
@ -9,15 +10,17 @@ import org.jetbrains.skia.Image
class ImageResourcesRepository( class ImageResourcesRepository(
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val googleImageConverter: ImagerModelConverter,
) { ) {
suspend fun load(url: String): ImageBitmap { suspend fun load(url: String): ImageBitmap {
try { try {
val byteArray = httpClient.get(url).readRawBytes() val unwrapUri = googleImageConverter.unwrap(model = url)
val byteArray = httpClient.get(unwrapUri).readRawBytes()
val skiaImage = Image.makeFromEncoded(byteArray) val skiaImage = Image.makeFromEncoded(byteArray)
return skiaImage.toComposeImageBitmap() return skiaImage.toComposeImageBitmap()
} catch (_: Exception) { } catch (_: Exception) {
// TODO proper exception handling (error bus ?) // TODO proper exception handling (error bus ?)
return ImageBitmap(width = 0, height = 0) return ImageBitmap(width = 1, height = 1)
} }
} }
} }

View file

@ -9,11 +9,17 @@ class ImagerModelConverter {
model: Any?, model: Any?,
): Any? { ): Any? {
return when (model) { return when (model) {
is String -> googleDriveUrlRegex.find(model)?.let { is String -> unwrap(model = model)
val id = it.groupValues.getOrNull(1)
"$workingGoogleDriveUri$id"
} ?: model
else -> model else -> model
} }
} }
fun unwrap(
model: String,
): String {
return googleDriveUrlRegex.find(model)?.let {
val id = it.groupValues.getOrNull(1)
"$workingGoogleDriveUri$id"
} ?: model
}
} }

View file

@ -102,7 +102,7 @@ fun CharacterRibbon(
) )
} }
CharacterRibbonRoll( CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value, roll = viewModel.roll(characterSheetId = it.characterSheetId),
) )
AnimatedVisibility( AnimatedVisibility(
visible = it.levelUp, visible = it.levelUp,

View file

@ -1,13 +1,6 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
@ -18,17 +11,18 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRollUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRollUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.RollEvent.Critical import com.pixelized.shared.lwa.protocol.websocket.RollEvent.Critical
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import java.text.Collator import java.text.Collator
@ -41,7 +35,7 @@ abstract class CharacterRibbonViewModel(
private val ribbonFactory: CharacterRibbonFactory, private val ribbonFactory: CharacterRibbonFactory,
) : ViewModel() { ) : ViewModel() {
private val rolls = hashMapOf<String, MutableState<CharacterRibbonRollUio?>>() private val rollFlowCache = hashMapOf<String, SharedFlow<CharacterRibbonRollUio?>>()
abstract fun fetch( abstract fun fetch(
campaign: Campaign, campaign: Campaign,
@ -119,40 +113,35 @@ abstract class CharacterRibbonViewModel(
.getOrNull(index) .getOrNull(index)
?.characterSheetId ?.characterSheetId
@Composable
@Stable @Stable
fun roll( fun roll(
characterSheetId: String, characterSheetId: String,
): State<CharacterRibbonRollUio?> { ): SharedFlow<CharacterRibbonRollUio?> {
val colorScheme = MaterialTheme.lwa.colorScheme return rollFlowCache.getOrPut(key = characterSheetId) {
val state = remember(characterSheetId) {
rolls.getOrPut(characterSheetId) { mutableStateOf(null) }
}
LaunchedEffect(characterSheetId) {
combine( combine(
settingsRepository.settingsFlow(), settingsRepository.settingsFlow(),
rollHistoryRepository.rolls(), rollHistoryRepository.rolls().filter { it.characterSheetId == characterSheetId },
) { settings, roll -> ) { settings, roll ->
if (settings.portrait.dynamicDice && characterSheetId == roll.characterSheetId) { if (settings.portrait.dynamicDice.not()) return@combine null
state.value = CharacterRibbonRollUio(
CharacterRibbonRollUio(
rollId = roll.uuid, rollId = roll.uuid,
hideDelay = settings.portrait.dynamicDiceDelay, hideDelay = settings.portrait.dynamicDiceDelay,
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
value = roll.rollValue, value = roll.rollValue,
tint = when (roll.critical) { critical = when (roll.critical) {
Critical.CRITICAL_SUCCESS -> colorScheme.portrait.criticalSuccess Critical.CRITICAL_SUCCESS -> CharacterRibbonRollUio.Critical.CRITICAL_SUCCESS
Critical.SPECIAL_SUCCESS -> colorScheme.portrait.spacialSuccess Critical.SPECIAL_SUCCESS -> CharacterRibbonRollUio.Critical.SPECIAL_SUCCESS
Critical.SUCCESS -> colorScheme.portrait.success Critical.SUCCESS -> CharacterRibbonRollUio.Critical.SUCCESS
Critical.FAILURE -> colorScheme.portrait.failure Critical.FAILURE -> CharacterRibbonRollUio.Critical.FAILURE
Critical.CRITICAL_FAILURE -> colorScheme.portrait.criticalFailure Critical.CRITICAL_FAILURE -> CharacterRibbonRollUio.Critical.CRITICAL_FAILURE
null -> colorScheme.portrait.default else -> null
}, },
) )
}.shareIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
)
} }
}.launchIn(this)
}
return state
} }
} }

View file

@ -23,16 +23,19 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.saveable.SaverScope import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.pixelized.desktop.lwa.ui.theme.color.LwaColors
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
@ -45,8 +48,17 @@ data class CharacterRibbonRollUio(
val hideDelay: Int, val hideDelay: Int,
val characterSheetId: String, val characterSheetId: String,
val value: Int?, val value: Int?,
val tint: Color?, val critical: Critical?,
) ) {
@Stable
enum class Critical {
CRITICAL_SUCCESS,
SPECIAL_SUCCESS,
SUCCESS,
FAILURE,
CRITICAL_FAILURE
}
}
@Stable @Stable
class CharacterRibbonRollAnimation( class CharacterRibbonRollAnimation(
@ -87,14 +99,30 @@ class CharacterRibbonRollAnimation(
fun CharacterRibbonRoll( fun CharacterRibbonRoll(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized, size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized,
value: CharacterRibbonRollUio?, roll: Flow<CharacterRibbonRollUio?>,
) {
val rollValue = roll.collectAsStateWithLifecycle(initialValue = null)
CharacterRibbonRoll(
modifier = modifier,
size = size,
roll = rollValue,
)
}
@Composable
fun CharacterRibbonRoll(
modifier: Modifier = Modifier,
size: DpSize = MaterialTheme.lwa.dimen.portrait.minimized,
portrait: LwaColors.Portrait = MaterialTheme.lwa.colorScheme.portrait,
roll: State<CharacterRibbonRollUio?>,
) { ) {
AnimatedContent( AnimatedContent(
modifier = modifier modifier = modifier
.width(width = size.width) .width(width = size.width)
.aspectRatio(ratio = 1f) .aspectRatio(ratio = 1f)
.graphicsLayer { clip = false }, .graphicsLayer { clip = false },
targetState = value, targetState = roll.value,
transitionSpec = { transitionSpec = {
val enter = fadeIn() val enter = fadeIn()
val exit = fadeOut() val exit = fadeOut()
@ -106,9 +134,15 @@ fun CharacterRibbonRoll(
rollDelay = it?.hideDelay ?: 1000, rollDelay = it?.hideDelay ?: 1000,
) )
val color = animateColorAsState( val color = animateColorAsState(
targetValue = it?.tint ?: Color.Transparent, targetValue = when (it?.critical) {
CharacterRibbonRollUio.Critical.CRITICAL_SUCCESS -> portrait.criticalSuccess
CharacterRibbonRollUio.Critical.SPECIAL_SUCCESS -> portrait.spacialSuccess
CharacterRibbonRollUio.Critical.SUCCESS -> portrait.success
CharacterRibbonRollUio.Critical.FAILURE -> portrait.failure
CharacterRibbonRollUio.Critical.CRITICAL_FAILURE -> portrait.criticalFailure
null -> portrait.default
}
) )
Box( Box(
modifier = Modifier.graphicsLayer { modifier = Modifier.graphicsLayer {
this.alpha = animation.animatedAlpha.value this.alpha = animation.animatedAlpha.value