diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/resources/ImageResourcesRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/resources/ImageResourcesRepository.kt index 454f9d9..686ec62 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/resources/ImageResourcesRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/resources/ImageResourcesRepository.kt @@ -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) } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/ImagerModelConverter.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/ImagerModelConverter.kt index 8172ad0..83ff6f8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/ImagerModelConverter.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/ImagerModelConverter.kt @@ -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 + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbon.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbon.kt index 35071d8..31c40af 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbon.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbon.kt @@ -102,7 +102,7 @@ fun CharacterRibbon( ) } CharacterRibbonRoll( - value = viewModel.roll(characterSheetId = it.characterSheetId).value, + roll = viewModel.roll(characterSheetId = it.characterSheetId), ) AnimatedVisibility( visible = it.levelUp, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt index 36f0bca..e581cf1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt @@ -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>() + private val rollFlowCache = hashMapOf>() abstract fun fetch( campaign: Campaign, @@ -119,40 +113,35 @@ abstract class CharacterRibbonViewModel( .getOrNull(index) ?.characterSheetId - @Composable @Stable fun roll( characterSheetId: String, - ): State { - val colorScheme = MaterialTheme.lwa.colorScheme - val state = remember(characterSheetId) { - rolls.getOrPut(characterSheetId) { mutableStateOf(null) } - } - - LaunchedEffect(characterSheetId) { + ): SharedFlow { + 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, + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt index 24753b8..c2d7c61 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt @@ -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, +) { + 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, ) { 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