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.toComposeImageBitmap
import com.pixelized.desktop.lwa.ui.composable.image.ImagerModelConverter
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.readRawBytes
@ -9,15 +10,17 @@ import org.jetbrains.skia.Image
class ImageResourcesRepository(
private val httpClient: HttpClient,
private val googleImageConverter: ImagerModelConverter,
) {
suspend fun load(url: String): ImageBitmap {
try {
val byteArray = httpClient.get(url).readRawBytes()
val unwrapUri = googleImageConverter.unwrap(model = url)
val byteArray = httpClient.get(unwrapUri).readRawBytes()
val skiaImage = Image.makeFromEncoded(byteArray)
return skiaImage.toComposeImageBitmap()
} catch (_: Exception) {
// 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?,
): Any? {
return when (model) {
is String -> googleDriveUrlRegex.find(model)?.let {
val id = it.groupValues.getOrNull(1)
"$workingGoogleDriveUri$id"
} ?: model
is String -> unwrap(model = 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(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
roll = viewModel.roll(characterSheetId = it.characterSheetId),
)
AnimatedVisibility(
visible = it.levelUp,

View file

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

View file

@ -23,16 +23,19 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
@ -45,8 +48,17 @@ data class CharacterRibbonRollUio(
val hideDelay: Int,
val characterSheetId: String,
val value: Int?,
val tint: Color?,
)
val critical: Critical?,
) {
@Stable
enum class Critical {
CRITICAL_SUCCESS,
SPECIAL_SUCCESS,
SUCCESS,
FAILURE,
CRITICAL_FAILURE
}
}
@Stable
class CharacterRibbonRollAnimation(
@ -87,14 +99,30 @@ class CharacterRibbonRollAnimation(
fun CharacterRibbonRoll(
modifier: Modifier = Modifier,
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(
modifier = modifier
.width(width = size.width)
.aspectRatio(ratio = 1f)
.graphicsLayer { clip = false },
targetState = value,
targetState = roll.value,
transitionSpec = {
val enter = fadeIn()
val exit = fadeOut()
@ -106,9 +134,15 @@ fun CharacterRibbonRoll(
rollDelay = it?.hideDelay ?: 1000,
)
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(
modifier = Modifier.graphicsLayer {
this.alpha = animation.animatedAlpha.value