Add client purse management.

This commit is contained in:
Thomas Andres Gomez 2025-04-13 10:58:35 +02:00
parent 4f33492b23
commit 8982bab22d
48 changed files with 1664 additions and 258 deletions

View file

@ -96,3 +96,7 @@ compose.desktop {
}
}
}
compose.resources {
generateResClass = auto
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M320,720L640,720L640,640L320,640L320,720ZM320,560L640,560L640,480L320,480L320,560ZM240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,360L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L520,360ZM240,160L240,160L240,360L240,360L240,160L240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M336,840Q245,840 182.5,777.5Q120,715 120,624Q120,586 133,550Q146,514 170,485L312,314L215,120L745,120L648,314L790,485Q814,514 827,550Q840,586 840,624Q840,715 777,777.5Q714,840 624,840L336,840ZM480,640Q447,640 423.5,616.5Q400,593 400,560Q400,527 423.5,503.5Q447,480 480,480Q513,480 536.5,503.5Q560,527 560,560Q560,593 536.5,616.5Q513,640 480,640ZM385,280L575,280L615,200L345,200L385,280ZM336,760L624,760Q681,760 720.5,720.5Q760,681 760,624Q760,600 751.5,577.5Q743,555 728,537L581,360L380,360L232,536Q217,554 208.5,577Q200,600 200,624Q200,681 239.5,720.5Q279,760 336,760Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M200,520L200,440L760,440L760,520L200,520Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -3,6 +3,28 @@
<string name="generic__arrow">&#x25B8;</string>
<string name="generic__gold__singular">Écu</string>
<string name="generic__silver__singular">Écusson</string>
<string name="generic__copper__singular">Liard</string>
<plurals name="generic__gold">
<item quantity="zero">Écu</item>
<item quantity="one">Écu</item>
<item quantity="other">Écus</item>
</plurals>
<plurals name="generic__silver">
<item quantity="zero">Écusson</item>
<item quantity="one">Écusson</item>
<item quantity="other">Écussons</item>
</plurals>
<plurals name="generic__copper">
<item quantity="zero">Liard</item>
<item quantity="one">Liard</item>
<item quantity="other">Liards</item>
</plurals>
<string name="error__missing_character_sheet__label">La feuille de personnage est introuvable</string>
<string name="error__default__action">Ok</string>
@ -162,6 +184,9 @@
<string name="character_sheet__delete_dialog__title">Supprimer la feuille de personnage</string>
<string name="character_sheet__delete_dialog__description">Êtes-vous sûr de vouloir supprimer "%1$s" ?</string>
<string name="character__inventory__add_to_purse__title">Ajouter à la bourse</string>
<string name="character__inventory__remove_from_purse__title">Retirer de la bourse</string>
<string name="tooltip__characteristics__characteristics">Les caractéristiques constituent les aptitudes innées dun personnage comme son intelligence, sa force, son charisme, etc. Elles ne sont pas acquises, mais peuvent être parfois augmentées par un entraînement ou une utilisation réussie. Les caractéristiques des humains normaux varient de 2 (niveau extrêmement bas) à 20 (maximum du potentiel humain), avec une moyenne de 10 ou 11. Plus une caractéristique est élevée plus le personnage est puissant dans cette aptitude.\nÀ la création de votre personnage, répartissez les valeurs suivantes dans les différentes caractéristiques : 15, 15, 13, 11, 10, 9 et 7.</string>
<string name="tooltip__characteristics__strength">La Force représente essentiellement la puissance musculaire du personnage. Elle ne décrit pas nécessairement la masse musculaire brute, mais lefficacité avec laquelle le personnage exerce ses muscles pour accomplir des actions physiques pénibles.\n\n- Bonus aux dégats\n- Réflexe\n- Athlétisme\n- Lancer\n- Saisie</string>
<string name="tooltip__characteristics__constitution">La Constitution est une mesure de la ténacité et de la résilience du personnage. Elle sert à résister aux maladies. Mais son aspect le plus important réside dans la détermination du nombre de dommages quun personnage peut supporter avant de succomber.\n\n- Point de vie maximum\n- Athlétisme\n- Acrobatie</string>

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository
import com.pixelized.desktop.lwa.repository.item.ItemRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.onEach
class DataSyncViewModel(
private val characterRepository: CharacterSheetRepository,
private val inventoryRepository: InventoryRepository,
private val alterationRepository: AlterationRepository,
private val itemRepository: ItemRepository,
private val campaignRepository: CampaignRepository,
@ -65,6 +67,9 @@ class DataSyncViewModel(
characterRepository.updateCharacterSheet(
characterSheetId = characterSheetId,
)
inventoryRepository.updateInventoryFlow(
characterSheetId = characterSheetId,
)
}
}
.launchIn(this)

View file

@ -8,6 +8,8 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignStore
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore
import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository
import com.pixelized.desktop.lwa.repository.inventory.InventoryStore
import com.pixelized.desktop.lwa.repository.item.ItemRepository
import com.pixelized.desktop.lwa.repository.item.ItemStore
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
@ -23,13 +25,17 @@ import com.pixelized.desktop.lwa.ui.composable.character.characteristic.Characte
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogViewModel
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlayViewModel
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
@ -112,6 +118,7 @@ val storeDependencies
singleOf(::CampaignStore)
singleOf(::TagStore)
singleOf(::ItemStore)
singleOf(::InventoryStore)
}
val repositoryDependencies
@ -124,6 +131,7 @@ val repositoryDependencies
singleOf(::CampaignRepository)
singleOf(::TagRepository)
singleOf(::ItemRepository)
singleOf(::InventoryRepository)
}
val factoryDependencies
@ -135,10 +143,13 @@ val factoryDependencies
factoryOf(::SettingsFactory)
factoryOf(::CampaignJsonFactory)
factoryOf(::CharacterRibbonFactory)
factoryOf(::CharacterDetailFactory)
factoryOf(::CharacterDetailHeaderFactory)
factoryOf(::CharacterDetailSheetFactory)
factoryOf(::CharacterDetailInventoryFactory)
factoryOf(::CharacterSheetCharacteristicDialogFactory)
factoryOf(::CharacterSheetDiminishedDialogFactory)
factoryOf(::CharacterSheetAlterationDialogFactory)
factoryOf(::PurseDialogFactory)
factoryOf(::TextMessageFactory)
factoryOf(::LevelUpFactory)
factoryOf(::GMTagFactory)
@ -160,10 +171,11 @@ val viewModelDependencies
viewModelOf(::NetworkViewModel)
viewModelOf(::PlayerRibbonViewModel)
viewModelOf(::NpcRibbonViewModel)
viewModelOf(::CharacterDetailViewModel)
viewModelOf(::CharacterDetailPanelViewModel)
viewModelOf(::CharacterSheetDiminishedDialogViewModel)
viewModelOf(::CharacterSheetCharacteristicDialogViewModel)
viewModelOf(::CharacterSheetAlterationDialogViewModel)
viewModelOf(::PurseDialogViewModel)
viewModelOf(::CampaignChatViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::LevelUpViewModel)

View file

@ -3,6 +3,8 @@ package com.pixelized.desktop.lwa.network
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.item.ItemJson
import com.pixelized.shared.lwa.model.tag.TagJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse
@ -102,6 +104,21 @@ interface LwaClient {
itemId: String,
): APIResponse<Unit>
// Inventory
suspend fun getInventory(
characterSheetId: String,
): APIResponse<InventoryJson>
suspend fun putInventory(
json: InventoryJson,
create: Boolean,
): APIResponse<Unit>
suspend fun deleteInventory(
characterSheetId: String,
): APIResponse<Unit>
// Tags
suspend fun getAlterationTags(): APIResponse<List<TagJson>>

View file

@ -4,6 +4,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.item.ItemJson
import com.pixelized.shared.lwa.model.tag.TagJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse
@ -175,6 +176,27 @@ class LwaClientImpl(
.delete("$root/item/delete?itemId=$itemId")
.body()
@Throws
override suspend fun getInventory(characterSheetId: String): APIResponse<InventoryJson> = client
.get("$root/inventory/detail?characterSheetId=$characterSheetId")
.body()
@Throws
override suspend fun putInventory(
inventory: InventoryJson,
create: Boolean,
): APIResponse<Unit> = client
.put("$root/inventory/update?create=$create") {
contentType(ContentType.Application.Json)
setBody(inventory)
}
.body<APIResponse<Unit>>()
@Throws
override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client
.delete("$root/inventory/delete?characterSheetId=$characterSheetId")
.body()
@Throws
override suspend fun getAlterationTags(): APIResponse<List<TagJson>> = client
.get("$root/tag/alteration")

View file

@ -0,0 +1,47 @@
package com.pixelized.desktop.lwa.repository.inventory
import com.pixelized.shared.lwa.model.inventory.Inventory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class InventoryRepository(
private val inventoryStore: InventoryStore,
) {
val inventoryFlow get() = inventoryStore.inventories
suspend fun updateInventoryFlow(characterSheetId: String) {
inventoryStore.updateInventoryFlow(characterSheetId = characterSheetId)
}
fun inventory(
characterSheetId: String?,
): Inventory? {
return inventoryFlow.value[characterSheetId]
}
fun inventoryFlow(
characterSheetId: String?,
): Flow<Inventory?> {
return inventoryFlow.map { it[characterSheetId] }
}
@Throws
suspend fun updateInventory(
inventory: Inventory,
create: Boolean,
) {
inventoryStore.putInventory(
inventory = inventory,
create = create,
)
}
@Throws
suspend fun deleteItem(
characterSheetId: String,
) {
inventoryStore.deleteInventory(
characterSheetId = characterSheetId,
)
}
}

View file

@ -0,0 +1,107 @@
package com.pixelized.desktop.lwa.repository.inventory
import com.pixelized.desktop.lwa.network.LwaClient
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class InventoryStore(
private val network: NetworkRepository,
private val factory: InventoryJsonFactory,
private val client: LwaClient,
) {
private val _inventories = MutableStateFlow<Map<String, Inventory>>(emptyMap())
val inventories: StateFlow<Map<String, Inventory>> = _inventories
init {
val scope = CoroutineScope(Dispatchers.IO + Job())
// data update through WebSocket.
scope.launch {
network.data.collect(::handleMessage)
}
}
suspend fun updateInventoryFlow(
characterSheetId: String,
) {
val inventory = try {
getInventory(characterSheetId = characterSheetId)
} catch (exception: Exception) {
println(exception.message) // TODO proper exception handling
null
}
// update the flow with the new item.
_inventories.update { items ->
items.toMutableMap().also {
it[characterSheetId] = inventory
?: Inventory.empty(characterSheetId = characterSheetId)
}
}
}
@Throws
private suspend fun getInventory(
characterSheetId: String,
): Inventory? {
val request = client.getInventory(characterSheetId = characterSheetId)
return when (request.success) {
true -> request.data?.let { factory.convertFromJson(json = it) }
else -> LwaClient.error(error = request)
}
}
@Throws
suspend fun putInventory(
inventory: Inventory,
create: Boolean,
) {
val request = client.putInventory(
json = factory.convertToJson(inventory = inventory),
create = create,
)
if (request.success.not()) {
LwaClient.error(error = request)
}
}
@Throws
suspend fun deleteInventory(
characterSheetId: String,
) {
val request = client.deleteInventory(characterSheetId = characterSheetId)
if (request.success.not()) {
LwaClient.error(error = request)
}
}
// region: WebSocket & data update.
private suspend fun handleMessage(message: SocketMessage) {
when (message) {
is ApiSynchronisation.InventoryApiSynchronisation -> when (message) {
is ApiSynchronisation.InventoryUpdate -> updateInventoryFlow(
characterSheetId = message.characterSheetId,
)
is ApiSynchronisation.InventoryDelete -> _inventories.update { items ->
items.toMutableMap().also {
it.remove(message.characterSheetId)
}
}
}
else -> Unit
}
}
// endregion
}

View file

@ -96,7 +96,10 @@ private fun CharacterSheetCharacteristicContent(
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(event = onDismissRequest)
.onPreviewEscape(
escape = onDismissRequest,
enter = { onConfirm(dialog) },
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,

View file

@ -88,7 +88,10 @@ private fun CharacterSheetDiminishedContent(
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(event = onDismissRequest)
.onPreviewEscape(
escape = onDismissRequest,
enter = { onConfirm(dialog) },
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,

View file

@ -0,0 +1,254 @@
package com.pixelized.desktop.lwa.ui.composable.character.purse
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape
import kotlinx.coroutines.flow.StateFlow
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action
import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action
import lwacharactersheet.composeapp.generated.resources.ic_remove_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable
data class PurseDialogUio(
val characterSheetId: String,
val label: StateFlow<String>,
val add: StateFlow<Boolean>,
val gold: LwaTextFieldUio,
val silver: LwaTextFieldUio,
val copper: LwaTextFieldUio,
val enableConfirm: StateFlow<Boolean>,
)
@Composable
fun PurseDialog(
dialog: State<PurseDialogUio?>,
onConfirm: (PurseDialogUio) -> Unit,
onSwapSign: (PurseDialogUio) -> Unit,
onDismissRequest: () -> Unit,
) {
dialog.value?.let {
Dialog(
onDismissRequest = onDismissRequest,
content = {
PurseContent(
dialog = it,
onConfirm = onConfirm,
onSwapSign = onSwapSign,
onDismissRequest = onDismissRequest,
)
PurseDialogKeyHandler(
onSwap = { onSwapSign(it) },
)
}
)
}
}
@Composable
fun PurseContent(
dialog: PurseDialogUio,
onConfirm: (PurseDialogUio) -> Unit,
onSwapSign: (PurseDialogUio) -> Unit,
onDismissRequest: () -> Unit,
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.requestFocus() }
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(
escape = onDismissRequest,
enter = { onConfirm(dialog) },
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,
) {
DecoratedBox {
Surface {
Column(
modifier = Modifier.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AnimatedContent(
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
targetState = dialog.label.collectAsState().value,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) {
Text(
style = MaterialTheme.typography.caption,
text = it,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.Bottom,
) {
SignButton(
modifier = Modifier
.size(size = 56.dp)
.background(
color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
shape = MaterialTheme.shapes.small,
),
add = dialog.add,
onClick = { onSwapSign(dialog) },
)
LwaTextField(
modifier = Modifier.focusRequester(focusRequester = focusRequester)
.width(100.dp),
field = dialog.gold,
)
LwaTextField(
modifier = Modifier.width(100.dp),
field = dialog.silver,
)
LwaTextField(
modifier = Modifier.width(100.dp),
field = dialog.copper,
)
}
Row(
modifier = Modifier
.padding(bottom = 4.dp)
.align(alignment = Alignment.End),
horizontalArrangement = Arrangement.spacedBy(
space = 4.dp,
alignment = Alignment.End
)
) {
TextButton(
onClick = onDismissRequest,
) {
Text(
color = MaterialTheme.colors.primary.copy(alpha = .7f),
text = stringResource(Res.string.dialog__cancel_action)
)
}
TextButton(
enabled = dialog.enableConfirm.collectAsState().value,
onClick = { onConfirm(dialog) },
) {
Text(
text = stringResource(Res.string.dialog__confirm_action)
)
}
}
}
}
}
}
}
@Composable
private fun SignButton(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource? = null,
enabled: Boolean = true,
add: StateFlow<Boolean>,
onClick: () -> Unit,
) {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
interactionSource = interactionSource,
) {
val rotation = animateFloatAsState(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
targetValue = when (add.collectAsState().value) {
true -> 90f
else -> 0f
}
)
Icon(
modifier = Modifier.graphicsLayer {
this.rotationZ = rotation.value * 2f
},
painter = painterResource(Res.drawable.ic_remove_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null
)
Icon(
modifier = Modifier.graphicsLayer {
this.rotationZ = rotation.value * 3f
},
painter = painterResource(Res.drawable.ic_remove_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null
)
}
}
@Composable
private fun PurseDialogKeyHandler(
onSwap: () -> Unit,
) {
KeyHandler {
if (it.type == KeyEventType.KeyDown) {
when (it.key) {
Key.AltLeft, Key.AltRight -> {
onSwap()
true
}
else -> false
}
} else {
false
}
}
}

View file

@ -0,0 +1,91 @@
package com.pixelized.desktop.lwa.ui.composable.character.purse
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__add_to_purse__title
import lwacharactersheet.composeapp.generated.resources.character__inventory__remove_from_purse__title
import lwacharactersheet.composeapp.generated.resources.generic__copper__singular
import lwacharactersheet.composeapp.generated.resources.generic__gold__singular
import lwacharactersheet.composeapp.generated.resources.generic__silver__singular
import org.jetbrains.compose.resources.getString
class PurseDialogFactory {
private val decimalChecker = Regex("""^\s*\d*\s*${'$'}""")
suspend fun convertToDialogUio(
scope: CoroutineScope,
characterSheetId: String?,
signFlow: StateFlow<Boolean>,
): PurseDialogUio? {
if (characterSheetId == null) return null
val gold = createFlows(initialValue = "")
.createCoinTextField(label = getString(Res.string.generic__gold__singular))
val silver = createFlows(initialValue = "")
.createCoinTextField(label = getString(Res.string.generic__silver__singular))
val copper = createFlows(initialValue = "")
.createCoinTextField(label = getString(Res.string.generic__copper__singular))
return PurseDialogUio(
characterSheetId = characterSheetId,
label = signFlow.map {
when (it) {
true -> getString(Res.string.character__inventory__add_to_purse__title)
false -> getString(Res.string.character__inventory__remove_from_purse__title)
}
}.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = getString(Res.string.character__inventory__add_to_purse__title),
),
add = signFlow,
gold = gold,
silver = silver,
copper = copper,
enableConfirm = combine(
gold.isError,
silver.isError,
copper.isError
) { goldError, silverError, copperError ->
!goldError && !silverError && !copperError
}.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = !gold.isError.value && !silver.isError.value && !copper.isError.value,
),
)
}
private fun check(value: String): Boolean = !decimalChecker.matches(value)
private fun Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>>.createCoinTextField(
label: String,
): LwaTextFieldUio {
val (valueFlow, errorFlow) = this
return LwaTextFieldUio(
enable = true,
isError = errorFlow,
valueFlow = valueFlow,
label = label,
placeHolder = null,
onValueChange = {
errorFlow.value = check(it)
valueFlow.value = it
},
)
}
private fun createFlows(
initialValue: String,
initialError: Boolean = false,
): Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>> {
return MutableStateFlow(initialValue) to MutableStateFlow(initialError)
}
}

View file

@ -0,0 +1,91 @@
package com.pixelized.desktop.lwa.ui.composable.character.purse
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import com.pixelized.shared.lwa.model.inventory.Inventory
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class PurseDialogViewModel(
private val inventoryRepository: InventoryRepository,
private val factory: PurseDialogFactory,
) : ViewModel() {
private val characterSheetIdFlow = MutableStateFlow<String?>(null)
private val signFlow = MutableStateFlow(true)
private val _error = MutableSharedFlow<ErrorSnackUio>()
val error: MutableSharedFlow<ErrorSnackUio> = _error
val purseDialog = characterSheetIdFlow.map { characterSheetId ->
factory.convertToDialogUio(
scope = viewModelScope,
characterSheetId = characterSheetId,
signFlow = signFlow,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = null,
)
fun showPurseDialog(
characterSheetId: String?,
) {
signFlow.value = true
characterSheetIdFlow.value = characterSheetId
}
fun hidePurseDialog() {
characterSheetIdFlow.value = null
}
fun swapPurseSign() {
signFlow.update { it.not() }
}
suspend fun confirmPurse(dialog: PurseDialogUio): Boolean {
// guard case: check if the dialog confirm state is enable
if (dialog.enableConfirm.value.not()) {
return false
}
// Get the player inventory
val inventory = inventoryRepository
.inventory(characterSheetId = dialog.characterSheetId)
?: return false
// compute the new purse
val sign = if (dialog.add.value) 1 else -1
val goldValue = dialog.gold.valueFlow.value.toIntOrNull() ?: 0
val silverValue = dialog.silver.valueFlow.value.toIntOrNull() ?: 0
val copperValue = dialog.copper.valueFlow.value.toIntOrNull() ?: 0
val purse = Inventory.Purse(
gold = inventory.purse.gold + goldValue * sign,
silver = inventory.purse.silver + silverValue * sign,
copper = inventory.purse.copper + copperValue * sign,
)
// guard case: check if the purse change, not an error case, but avoid useless API call.
if (inventory.purse == purse) {
return true
}
// API call.
return try {
inventoryRepository.updateInventory(
inventory = inventory.copy(
purse = purse
),
create = false,
)
true
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(value = message)
false
}
}
}

View file

@ -20,6 +20,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isAltPressed
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.onSizeChanged
@ -43,10 +45,12 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChat
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar
@ -60,8 +64,9 @@ val LocalCampaignLayoutScope = compositionLocalOf<CampaignLayoutScope> {
@Composable
fun CampaignScreen(
playerDetailViewModel: CharacterDetailViewModel = koinViewModel(key = "player"),
npcDetailViewModel: CharacterDetailViewModel = koinViewModel(key = "npc"),
playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(),
playerDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "player"),
npcDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "npc"),
characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterSheetDiminishedDialogViewModel = koinViewModel(),
alterationViewModel: CharacterSheetAlterationDialogViewModel = koinViewModel(),
@ -111,9 +116,45 @@ fun CampaignScreen(
leftPanel = {
PlayerRibbon(
modifier = Modifier.fillMaxHeight(),
onCharacter = {
viewModel = playerRibbonViewModel,
onCharacterLeftClick = {
scope.launch {
playerDetailViewModel.showCharacter(characterSheetId = it)
playerDetailViewModel.showCharacter(
characterSheetId = it,
panel = DetailPanelUio.Sheet,
)
}
},
onCharacterRightClick = {
scope.launch {
playerDetailViewModel.showCharacter(
characterSheetId = it,
panel = DetailPanelUio.Inventory,
)
}
},
onLevelUp = {
screen.navigateToLevelScreen(characterSheetId = it)
}
)
},
rightPanel = {
NpcRibbon(
modifier = Modifier.fillMaxHeight(),
onCharacterLeftClick = {
scope.launch {
npcDetailViewModel.showCharacter(
characterSheetId = it,
panel = DetailPanelUio.Sheet,
)
}
},
onCharacterRightClick = {
scope.launch {
npcDetailViewModel.showCharacter(
characterSheetId = it,
panel = DetailPanelUio.Inventory,
)
}
},
onLevelUp = {
@ -129,7 +170,7 @@ fun CampaignScreen(
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr),
blurController = blurController,
detailViewModel = npcDetailViewModel,
detailPanelViewModel = npcDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel,
@ -138,19 +179,6 @@ fun CampaignScreen(
}
)
},
rightPanel = {
NpcRibbon(
modifier = Modifier.fillMaxHeight(),
onCharacter = {
scope.launch {
npcDetailViewModel.showCharacter(characterSheetId = it)
}
},
onLevelUp = {
screen.navigateToLevelScreen(characterSheetId = it)
}
)
},
rightOverlay = {
CharacterDetailPanel(
modifier = Modifier
@ -159,7 +187,7 @@ fun CampaignScreen(
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
blurController = blurController,
detailViewModel = playerDetailViewModel,
detailPanelViewModel = playerDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel,
@ -233,6 +261,26 @@ fun CampaignScreen(
playerDetailViewModel.hideCharacter()
npcDetailViewModel.hideCharacter()
}
},
onPlayerNumber = {
scope.launch {
val characterSheetId = playerRibbonViewModel.characters(index = it) ?: return@launch
npcDetailViewModel.hideCharacter()
playerDetailViewModel.showCharacter(
characterSheetId = characterSheetId,
panel = DetailPanelUio.Sheet,
)
}
},
onAltPLayerNumber = {
scope.launch {
val characterSheetId = playerRibbonViewModel.characters(index = it) ?: return@launch
npcDetailViewModel.hideCharacter()
playerDetailViewModel.showCharacter(
characterSheetId = characterSheetId,
panel = DetailPanelUio.Inventory,
)
}
}
)
@ -339,16 +387,30 @@ private fun CampaignLayout(
@Composable
private fun CampaignKeyHandler(
onDismissRequest: () -> Unit,
onPlayerNumber: (index: Int) -> Unit,
onAltPLayerNumber: (index: Int) -> Unit,
) {
KeyHandler {
when {
it.type == KeyEventType.KeyDown && it.key == Key.Escape -> {
onDismissRequest()
true
}
else -> false
if (it.type != KeyEventType.KeyDown) return@KeyHandler false
if (it.key == Key.Escape) {
onDismissRequest()
return@KeyHandler true
}
if (it.isCtrlPressed.not()) return@KeyHandler false
when (it.key) {
Key.Escape -> onDismissRequest()
Key.One, Key.NumPad1 -> if (it.isAltPressed) onAltPLayerNumber(0) else onPlayerNumber(0)
Key.Two, Key.NumPad2 -> if (it.isAltPressed) onAltPLayerNumber(1) else onPlayerNumber(1)
Key.Three, Key.NumPad3 -> if (it.isAltPressed) onAltPLayerNumber(2) else onPlayerNumber(2)
Key.Four, Key.NumPad4 -> if (it.isAltPressed) onAltPLayerNumber(3) else onPlayerNumber(3)
Key.Five, Key.NumPad5 -> if (it.isAltPressed) onAltPLayerNumber(4) else onPlayerNumber(4)
Key.Six, Key.NumPad6 -> if (it.isAltPressed) onAltPLayerNumber(5) else onPlayerNumber(5)
Key.Seven, Key.NumPad7 -> if (it.isAltPressed) onAltPLayerNumber(6) else onPlayerNumber(6)
Key.Eight, Key.NumPad8 -> if (it.isAltPressed) onAltPLayerNumber(7) else onPlayerNumber(7)
Key.Nine, Key.NumPad9 -> if (it.isAltPressed) onAltPLayerNumber(8) else onPlayerNumber(8)
else -> return@KeyHandler false
}
return@KeyHandler true
}
}

View file

@ -9,8 +9,9 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
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.Column
@ -23,6 +24,7 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.onClick
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
@ -36,6 +38,7 @@ import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
@ -69,13 +72,15 @@ data class CharacterPortraitUio(
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CharacterPortrait(
modifier: Modifier = Modifier,
size: DpSize = MaterialTheme.lwa.size.portrait.minimized,
levelUpOffset: Dp = 9.dp,
character: CharacterPortraitUio,
onCharacter: (characterSheetId: String) -> Unit,
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val colorScheme = MaterialTheme.lwa.colorScheme
@ -86,7 +91,16 @@ fun CharacterPortrait(
.size(size = size)
.clip(shape = MaterialTheme.lwa.shapes.portrait)
.background(color = colorScheme.elevated.base1dp)
.clickable(character.enableDetail) { onCharacter(character.characterSheetId) },
.onClick(
matcher = PointerMatcher.mouse(PointerButton.Primary),
enabled = character.enableDetail,
onClick = { onCharacterLeftClick(character.characterSheetId) }
)
.onClick(
matcher = PointerMatcher.mouse(PointerButton.Secondary),
enabled = character.enableDetail,
onClick = { onCharacterRightClick(character.characterSheetId) }
),
) {
AnimatedContent(
targetState = character.portrait,

View file

@ -16,11 +16,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
@ -37,31 +39,44 @@ import com.pixelized.desktop.lwa.ui.composable.character.characteristic.Characte
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeader
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheet
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetActionUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetActionUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetSkillUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@Stable
data class CharacterDetailPanelUio(
val characterSheetId: String?,
val panel: DetailPanelUio,
val header: StateFlow<CharacterDetailHeaderUio?>,
val sheet: StateFlow<CharacterDetailSheetUio?>,
val inventory: StateFlow<CharacterDetailInventoryUio?>,
)
@Stable
enum class DetailPanelUio {
Sheet,
Inventory,
}
@Composable
fun CharacterDetailPanel(
modifier: Modifier = Modifier,
blurController: BlurContentController,
transitionSpec: AnimatedContentTransitionScope<CharacterDetailPanelUio>.() -> ContentTransform = rememberTransitionAnimation(),
detailViewModel: CharacterDetailViewModel,
detailPanelViewModel: CharacterDetailPanelViewModel,
characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel,
characterDiminishedViewModel: CharacterSheetDiminishedDialogViewModel,
alterationViewModel: CharacterSheetAlterationDialogViewModel,
@ -69,16 +84,42 @@ fun CharacterDetailPanel(
) {
val roll = LocalRollHostState.current
val scope = rememberCoroutineScope()
val detail: State<CharacterDetailPanelUio> = detailViewModel.detail.collectAsState()
val pager = rememberPagerState(initialPage = 0) { DetailPanelUio.entries.size }
val detail: State<CharacterDetailPanelUio> = detailPanelViewModel.detail.collectAsState()
LaunchedEffect(Unit) {
detailPanelViewModel.detail
.map { it.panel }
.distinctUntilChanged()
.onEach { pager.animateScrollToPage(page = it.ordinal) }
.launchIn(this)
}
CharacterDetailAnimatedPanel(
modifier = modifier,
pagerState = pager,
detail = detail,
transitionSpec = transitionSpec,
onDismissRequest = {
detailViewModel.hideCharacter()
detailPanelViewModel.hideCharacter()
},
onLevelUp = onLevelUp,
onSheet = { characterSheetId ->
scope.launch {
detailPanelViewModel.showCharacter(
characterSheetId = characterSheetId,
panel = DetailPanelUio.Sheet,
)
}
},
onInventory = { characterSheetId ->
scope.launch {
detailPanelViewModel.showCharacter(
characterSheetId = characterSheetId,
panel = DetailPanelUio.Inventory,
)
}
},
onAlteration = {
blurController.show()
alterationViewModel.show(characterSheetId = it)
@ -124,7 +165,7 @@ fun CharacterDetailPanel(
onSkill = {
scope.launch {
val result = roll.showRollOverlay(roll = it.roll)
detailViewModel.onSkillUse(
detailPanelViewModel.onSkillUse(
skillId = it.skillId,
result = result,
)
@ -133,7 +174,7 @@ fun CharacterDetailPanel(
},
onUseSkill = {
scope.launch {
detailViewModel.onSkillUse(
detailPanelViewModel.onSkillUse(
skillId = it.skillId,
used = it.used,
)
@ -151,10 +192,14 @@ fun CharacterDetailPanel(
@Composable
fun CharacterDetailAnimatedPanel(
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.lwa.shapes.panel,
pagerState: PagerState,
detail: State<CharacterDetailPanelUio>,
transitionSpec: AnimatedContentTransitionScope<CharacterDetailPanelUio>.() -> ContentTransform,
onDismissRequest: (characterSheetId: String) -> Unit,
onDismissRequest: () -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
onSheet: (characterSheetId: String) -> Unit,
onInventory: (characterSheetId: String) -> Unit,
onAlteration: (characterSheetId: String) -> Unit,
onDiminished: (characterSheetId: String) -> Unit,
onHp: (characterSheetId: String) -> Unit,
@ -184,10 +229,13 @@ fun CharacterDetailAnimatedPanel(
) {
CharacterDetailContent(
modifier = Modifier.matchParentSize(),
header = it.header.collectAsState(),
sheet = it.sheet.collectAsState(),
onDismissRequest = { onDismissRequest(it.characterSheetId) },
pagerState = pagerState,
detail = it,
shape = shape,
onDismissRequest = onDismissRequest,
onLevelUp = { onLevelUp(it.characterSheetId) },
onSheet = { onSheet(it.characterSheetId) },
onInventory = { onInventory(it.characterSheetId) },
onAlteration = { onAlteration(it.characterSheetId) },
onDiminished = { onDiminished(it.characterSheetId) },
onHp = { onHp(it.characterSheetId) },
@ -209,9 +257,11 @@ fun CharacterDetailAnimatedPanel(
fun CharacterDetailContent(
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.lwa.shapes.panel,
header: State<CharacterDetailHeaderUio?>,
sheet: State<CharacterDetailSheetUio?>,
detail: CharacterDetailPanelUio,
pagerState: PagerState,
onLevelUp: () -> Unit,
onSheet: () -> Unit,
onInventory: () -> Unit,
onAlteration: () -> Unit,
onDismissRequest: () -> Unit,
onDiminished: () -> Unit,
@ -224,7 +274,7 @@ fun CharacterDetailContent(
onAction: (CharacterDetailSheetActionUio) -> Unit,
) {
Surface(
modifier = modifier.fillMaxSize(),
modifier = modifier,
shape = shape,
color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
) {
@ -232,29 +282,42 @@ fun CharacterDetailContent(
CharacterDetailHeader(
modifier = Modifier
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.padding(bottom = 8.dp)
.fillMaxWidth(),
header = header,
header = detail.header.collectAsState(),
onDismissRequest = onDismissRequest,
onLevelUp = onLevelUp,
onSheet = onSheet,
onInventory = onInventory,
onAlteration = onAlteration,
onDiminished = onDiminished,
onHp = onHp,
onPp = onPp,
onReflex = onReflex,
)
CharacterDetailSheet(
modifier = Modifier
.weight(1f)
.verticalScroll(state = rememberScrollState())
.padding(horizontal = 16.dp)
.padding(top = 8.dp, bottom = 16.dp),
sheet = sheet,
onCharacteristic = onCharacteristic,
onSkill = onSkill,
onUseSkill = onUseSkill,
onAction = onAction,
)
HorizontalPager(
modifier = Modifier.weight(1f),
state = pagerState,
) { page ->
when (page) {
DetailPanelUio.Sheet.ordinal -> {
CharacterDetailSheet(
modifier = Modifier.fillMaxSize(),
sheet = detail.sheet.collectAsState(),
onCharacteristic = onCharacteristic,
onSkill = onSkill,
onUseSkill = onUseSkill,
onAction = onAction,
)
}
DetailPanelUio.Inventory.ordinal -> {
CharacterDetailInventory(
modifier = Modifier.fillMaxSize(),
inventory = detail.inventory.collectAsState(),
)
}
}
}
}
}
}

View file

@ -2,87 +2,80 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.navigation.window.WindowController
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult
import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult.BoundedRollResult.Difficulty
import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult.BoundedRollResult.Result
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__edit__title
import org.jetbrains.compose.resources.getString
class CharacterDetailViewModel(
class CharacterDetailPanelViewModel(
private val characterSheetRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
settingRepository: SettingsRepository,
private val characterDetailFactory: CharacterDetailFactory,
private val characterHeaderFactory: CharacterDetailHeaderFactory,
private val characterSheetFactory: CharacterDetailSheetFactory,
private val characterInventoryFactory: CharacterDetailInventoryFactory,
private val network: NetworkRepository,
) : ViewModel() {
private val displayedCharacterId = MutableStateFlow<String?>(null)
private val characterSheetPanelFlow = MutableStateFlow<CharacterSheetPanel?>(null)
val detail: StateFlow<CharacterDetailPanelUio> = displayedCharacterId
.map { characterSheetId ->
if (characterSheetId == null) return@map empty()
CharacterDetailPanelUio(
val detail: StateFlow<CharacterDetailPanelUio> = characterSheetPanelFlow
.map { characterSheetPanel ->
val (characterSheetId, panel) = characterSheetPanel ?: return@map emptyPanel()
return@map CharacterDetailPanelUio(
characterSheetId = characterSheetId,
header = combine(
characterSheetRepository.characterDetailFlow(characterSheetId = characterSheetId),
alterationRepository.fieldAlterationsFlow(characterSheetId = characterSheetId),
settingRepository.settingsFlow()
) { characterSheet, alterations, settings ->
characterDetailFactory.convertToCharacterDetailHeaderUio(
characterSheetId = characterSheetId,
characterSheet = characterSheet,
settings = settings,
alterations = alterations,
)
}.stateIn(
panel = panel,
header = characterHeaderFactory.convertToCharacterDetailHeaderUioFlow(
characterSheetId = characterSheetId,
panel = panel,
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null,
initialValue = ::emptyHeader,
),
sheet = combine(
characterSheetRepository.characterDetailFlow(characterSheetId = characterSheetId),
alterationRepository.fieldAlterationsFlow(characterSheetId = characterSheetId),
) { characterSheet, alterations ->
characterDetailFactory.convertToCharacterDetailSheetUio(
characterSheetId = characterSheetId,
characterSheet = characterSheet,
alterations = alterations,
)
}.stateIn(
sheet = characterSheetFactory.convertToCharacterDetailSheetUioFlow(
characterSheetId = characterSheetId,
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null,
initialValue = ::emptySheet,
),
inventory = characterInventoryFactory.convertToCharacterInventoryUioFlow(
characterSheetId = characterSheetId,
scope = viewModelScope,
initialValue = ::emptyInventory,
),
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = empty(),
initialValue = emptyPanel(),
)
suspend fun showCharacter(
characterSheetId: String,
panel: DetailPanelUio,
) {
if (characterSheetRepository.characterDetail(characterSheetId = characterSheetId) == null) {
characterSheetRepository.updateCharacterSheet(characterSheetId = characterSheetId)
}
displayedCharacterId.value = characterSheetId
characterSheetPanelFlow.value = CharacterSheetPanel(
characterSheetId = characterSheetId,
panel = panel,
)
}
suspend fun editCharacter(
@ -99,14 +92,14 @@ class CharacterDetailViewModel(
}
fun hideCharacter() {
displayedCharacterId.value = null
characterSheetPanelFlow.value = null
}
suspend fun onSkillUse(
skillId: String,
result: RollResult,
) {
val characterSheetId = displayedCharacterId.value ?: return
val (characterSheetId, _) = characterSheetPanelFlow.value ?: return
// check if the RollResult is a BoundedRollResult. can work with other roll result.
val roll = result as? RollResult.BoundedRollResult ?: return
// check if the roll is a success with some challenge.
@ -136,7 +129,7 @@ class CharacterDetailViewModel(
skillId: String,
used: Boolean,
) {
val characterSheetId = displayedCharacterId.value ?: return
val (characterSheetId, _) = characterSheetPanelFlow.value ?: return
network.share(
message = CharacterSheetEvent.UpdateSkillUsageEvent(
timestamp = System.currentTimeMillis(),
@ -147,9 +140,22 @@ class CharacterDetailViewModel(
)
}
private fun empty() = CharacterDetailPanelUio(
private fun emptyPanel() = CharacterDetailPanelUio(
characterSheetId = null,
header = MutableStateFlow(null),
sheet = MutableStateFlow(null),
panel = DetailPanelUio.Sheet,
header = MutableStateFlow(value = emptyHeader()),
sheet = MutableStateFlow(value = emptySheet()),
inventory = MutableStateFlow(value = emptyInventory()),
)
private fun emptyHeader(): CharacterDetailHeaderUio? = null
private fun emptySheet(): CharacterDetailSheetUio? = null
private fun emptyInventory(): CharacterDetailInventoryUio? = null
data class CharacterSheetPanel(
val characterSheetId: String,
val panel: DetailPanelUio,
)
}

View file

@ -13,8 +13,11 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -26,9 +29,11 @@ import androidx.compose.material.Text
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.draw.clip
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -38,6 +43,7 @@ import com.pixelized.desktop.lwa.ui.composable.shapes.ArrowShape
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__level
@ -45,9 +51,11 @@ import lwacharactersheet.composeapp.generated.resources.ic_azm_24dp
import lwacharactersheet.composeapp.generated.resources.ic_blur_on_24dp
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import lwacharactersheet.composeapp.generated.resources.ic_cognition_24dp
import lwacharactersheet.composeapp.generated.resources.ic_description_24dp
import lwacharactersheet.composeapp.generated.resources.ic_directions_run_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_plus_24dp
import lwacharactersheet.composeapp.generated.resources.ic_money_bag_24dp
import lwacharactersheet.composeapp.generated.resources.ic_pan_tool_24dp
import lwacharactersheet.composeapp.generated.resources.ic_shield_24dp
import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp
@ -59,6 +67,7 @@ import org.jetbrains.compose.resources.stringResource
@Stable
data class CharacterDetailHeaderUio(
val characterSheetId: String,
val panel: DetailPanelUio,
val portrait: String?,
val diminished: Int,
val alteration: Boolean,
@ -88,25 +97,39 @@ data class CharacterDetailHeaderUio(
val initiativeTooltip: TooltipUio,
)
@Stable
object CharacterDetailHeaderDefault {
@Stable
val paddings = PaddingValues(start = 16.dp, end = 16.dp, bottom = 8.dp)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CharacterDetailHeader(
modifier: Modifier = Modifier,
paddings: PaddingValues = CharacterDetailHeaderDefault.paddings,
iconSize: Dp = MaterialTheme.lwa.size.sheet.subCategory,
header: State<CharacterDetailHeaderUio?>,
onDismissRequest: () -> Unit,
onLevelUp: () -> Unit,
onSheet: () -> Unit,
onInventory: () -> Unit,
onAlteration: () -> Unit,
onDiminished: () -> Unit,
onHp: () -> Unit,
onPp: () -> Unit,
onReflex: (RollAction.Uio) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val startPadding = remember(layoutDirection) { paddings.calculateStartPadding(layoutDirection) }
val endPadding = remember(layoutDirection) { paddings.calculateEndPadding(layoutDirection) }
val bottomPadding = remember { paddings.calculateBottomPadding() }
Column(
modifier = modifier,
) {
Row(
modifier = Modifier.padding(start = 16.dp),
modifier = Modifier.padding(start = startPadding),
) {
Row(
modifier = Modifier
@ -130,6 +153,50 @@ fun CharacterDetailHeader(
),
)
}
AnimatedVisibility(
visible = header.value?.levelUp == true,
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = onLevelUp,
) {
ArrowShape(
color = MaterialTheme.lwa.colorScheme.portrait.levelUp,
)
}
}
AnimatedContent(
targetState = header.value?.panel,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) {
when (it) {
DetailPanelUio.Inventory -> IconButton(
onClick = onSheet,
) {
Icon(
modifier = Modifier.size(size = 24.dp),
painter = painterResource(Res.drawable.ic_description_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
DetailPanelUio.Sheet -> IconButton(
onClick = onInventory,
) {
Icon(
modifier = Modifier.size(size = 24.dp),
painter = painterResource(Res.drawable.ic_money_bag_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
null -> Unit
}
}
AnimatedVisibility(
visible = header.value?.alteration == true,
enter = fadeIn(),
@ -146,19 +213,6 @@ fun CharacterDetailHeader(
)
}
}
AnimatedVisibility(
visible = header.value?.levelUp == true,
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = onLevelUp,
) {
ArrowShape(
color = MaterialTheme.lwa.colorScheme.portrait.levelUp,
)
}
}
Box {
IconButton(
onClick = onDiminished,
@ -200,7 +254,11 @@ fun CharacterDetailHeader(
}
}
Row(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(
start = startPadding,
end = endPadding,
bottom = bottomPadding,
),
horizontalArrangement = Arrangement.spacedBy(space = 10.dp),
) {
TooltipLayout(

View file

@ -0,0 +1,153 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.alteration.FieldAlteration
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.usecase.ExpressionUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__armor
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__damage_bonus
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__hit_point
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__hp_grow
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__initiative
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__learning
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__movement
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__power_point
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__reflex
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__armor
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__bonus_damage
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__hit_point
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__hp_grow
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__initiative
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__learning
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__movement
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__power_point
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__reflex
import org.jetbrains.compose.resources.getString
class CharacterDetailHeaderFactory(
private val characterSheetRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
private val settingRepository: SettingsRepository,
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
) {
fun convertToCharacterDetailHeaderUioFlow(
characterSheetId: String,
panel: DetailPanelUio,
scope: CoroutineScope,
started: SharingStarted = SharingStarted.Eagerly,
initialValue: () -> CharacterDetailHeaderUio?,
): StateFlow<CharacterDetailHeaderUio?> {
return combine(
characterSheetRepository.characterDetailFlow(characterSheetId = characterSheetId),
alterationRepository.fieldAlterationsFlow(characterSheetId = characterSheetId),
settingRepository.settingsFlow()
) { characterSheet, alterations, settings ->
convertToCharacterDetailHeaderUio(
characterSheetId = characterSheetId,
panel = panel,
characterSheet = characterSheet,
settings = settings,
alterations = alterations,
)
}.stateIn(
scope = scope,
started = started,
initialValue = initialValue(),
)
}
private suspend fun convertToCharacterDetailHeaderUio(
characterSheetId: String,
characterSheet: CharacterSheet?,
panel: DetailPanelUio,
settings: Settings,
alterations: Map<String, List<FieldAlteration>>,
): CharacterDetailHeaderUio? {
if (characterSheet == null) return null
val alteredCharacterSheet = alteredCharacterSheetFactory.sheet(
characterSheet = characterSheet,
alterations = alterations,
)
val maxHp = alteredCharacterSheet.maxHp
val maxPp = alteredCharacterSheet.maxPp
return CharacterDetailHeaderUio(
characterSheetId = characterSheetId,
panel = panel,
portrait = alteredCharacterSheet.portrait,
diminished = alteredCharacterSheet.diminished,
alteration = settings.isAdmin ?: false,
levelUp = alteredCharacterSheet.shouldLevelUp,
name = alteredCharacterSheet.name,
level = alteredCharacterSheet.level,
hp = "${maxHp - alteredCharacterSheet.damage}",
hpTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__hit_point),
description = getString(Res.string.tooltip__sub_characteristics__hit_point)
),
maxHp = "$maxHp",
pp = "${maxPp - alteredCharacterSheet.fatigue}",
ppTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__power_point),
description = getString(Res.string.tooltip__sub_characteristics__power_point)
),
maxPp = "$maxPp",
mov = "${alteredCharacterSheet.movement}",
movTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__movement),
description = getString(Res.string.tooltip__sub_characteristics__movement)
),
armor = "${alteredCharacterSheet.armor}",
armorTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__armor),
description = getString(Res.string.tooltip__sub_characteristics__armor)
),
bonus = alteredCharacterSheet.damageBonus,
bonusTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__damage_bonus),
description = getString(Res.string.tooltip__sub_characteristics__bonus_damage)
),
grow = "${alteredCharacterSheet.hpGrow}",
growTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__hp_grow),
description = getString(Res.string.tooltip__sub_characteristics__hp_grow)
),
learn = "${alteredCharacterSheet.learning}",
learnTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__learning),
description = getString(Res.string.tooltip__sub_characteristics__learning)
),
reflex = "${alteredCharacterSheet.reflex}",
reflexTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__reflex),
description = getString(Res.string.tooltip__sub_characteristics__reflex)
),
reflexRoll = RollAction.Uio.BoundedRollActionUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__sub_characteristics__reflex),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.reflex * 5,
),
initiative = "${alteredCharacterSheet.initiative}",
initiativeTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__initiative),
description = getString(Res.string.tooltip__sub_characteristics__initiative)
),
)
}
}

View file

@ -0,0 +1,236 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
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.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.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialog
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.InventoryItem
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.InventoryItemUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_copper_32px
import lwacharactersheet.composeapp.generated.resources.ic_gold_32px
import lwacharactersheet.composeapp.generated.resources.ic_silver_32px
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel
@Stable
data class CharacterDetailInventoryUio(
val characterSheetId: String,
val purse: PurseUio,
val items: List<InventoryItemUio>,
) {
@Stable
data class PurseUio(
val gold: Int,
val silver: Int,
val copper: Int,
)
}
object CharacterDetailInventoryDefault {
val padding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
}
@Composable
fun CharacterDetailInventory(
modifier: Modifier = Modifier,
paddings: PaddingValues = CharacterDetailInventoryDefault.padding,
purseViewModel: PurseDialogViewModel = koinViewModel(),
inventory: State<CharacterDetailInventoryUio?>,
) {
val blur = LocalBlurController.current
val scope = rememberCoroutineScope()
when (val unWrap = inventory.value) {
null -> Box(modifier = modifier)
else -> CharacterDetailInventoryContent(
modifier = modifier,
paddings = paddings,
inventory = unWrap,
onPurse = {
blur.show()
purseViewModel.showPurseDialog(characterSheetId = it)
}
)
}
PurseDialog(
dialog = purseViewModel.purseDialog.collectAsState(),
onDismissRequest = {
blur.hide()
purseViewModel.hidePurseDialog()
},
onSwapSign = {
purseViewModel.swapPurseSign()
},
onConfirm = {
scope.launch {
if (purseViewModel.confirmPurse(dialog = it)) {
blur.hide()
purseViewModel.hidePurseDialog()
}
}
}
)
ErrorSnackHandler(
error = purseViewModel.error,
)
}
@Composable
private fun CharacterDetailInventoryContent(
modifier: Modifier = Modifier,
paddings: PaddingValues,
inventory: CharacterDetailInventoryUio,
onPurse: (String) -> Unit,
) {
Column(
modifier = modifier,
) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = paddings,
) {
items(
items = inventory.items,
key = { it.inventoryId },
) {
InventoryItem(
modifier = Modifier.fillMaxWidth(),
item = it,
onClick = { },
)
}
}
Purse(
modifier = Modifier.fillMaxWidth(),
paddings = paddings,
purse = inventory.purse,
onPurse = { onPurse(inventory.characterSheetId) },
)
}
}
@Composable
private fun Purse(
modifier: Modifier = Modifier,
paddings: PaddingValues,
purse: CharacterDetailInventoryUio.PurseUio,
onPurse: () -> Unit,
) {
Row(
modifier = Modifier
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.then(other = modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
) {
Row(
modifier = Modifier
.clickable { onPurse() }
.padding(paddingValues = paddings),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_gold_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.gold,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_silver_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.silver,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_copper_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.copper,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
}
}
}
@Composable
@Stable
private fun coinTransitionSpec(): AnimatedContentTransitionScope<Int>.() -> ContentTransform = {
val enter = fadeIn() + slideInVertically { -16 }
val exit = fadeOut() + slideOutVertically { 16 }
enter togetherWith exit using SizeTransform(clip = false)
}

View file

@ -0,0 +1,68 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory
import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository
import com.pixelized.desktop.lwa.repository.item.ItemRepository
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.InventoryItemUio
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.item.Item
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
class CharacterDetailInventoryFactory(
private val inventoryRepository: InventoryRepository,
private val itemRepository: ItemRepository,
) {
fun convertToCharacterInventoryUioFlow(
characterSheetId: String,
scope: CoroutineScope,
started: SharingStarted = SharingStarted.Eagerly,
initialValue: () -> CharacterDetailInventoryUio?,
): StateFlow<CharacterDetailInventoryUio?> {
return combine(
inventoryRepository.inventoryFlow(characterSheetId = characterSheetId),
itemRepository.itemFlow,
) { inventory, items ->
convertToCharacterInventoryUio(
characterSheetId = characterSheetId,
purse = inventory?.purse,
inventory = inventory?.items,
items = items,
)
}.stateIn(
scope = scope,
started = started,
initialValue = initialValue(),
)
}
private suspend fun convertToCharacterInventoryUio(
characterSheetId: String?,
purse: Inventory.Purse?,
inventory: List<Inventory.Item>?,
items: Map<String, Item>,
): CharacterDetailInventoryUio? {
if (characterSheetId == null) return null
return CharacterDetailInventoryUio(
characterSheetId = characterSheetId,
purse = CharacterDetailInventoryUio.PurseUio(
gold = purse?.gold ?: 0,
silver = purse?.silver ?: 0,
copper = purse?.copper ?: 0,
),
items = inventory
?.mapNotNull {
val label = items[it.itemId]?.metadata?.name ?: return@mapNotNull null
InventoryItemUio(
inventoryId = it.inventoryId,
label = label,
)
}
?: emptyList()
)
}
}

View file

@ -0,0 +1,51 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
@Stable
data class InventoryItemUio(
val inventoryId: String,
val label: String,
)
@Stable
object GMCharacterPreviewDefault {
@Stable
val paddings = PaddingValues(horizontal = 16.dp)
}
@Composable
fun InventoryItem(
modifier: Modifier = Modifier,
padding: PaddingValues = GMCharacterPreviewDefault.paddings,
item: InventoryItemUio,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.clip(shape = MaterialTheme.lwa.shapes.gameMaster)
.clickable(onClick = onClick)
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.minimumInteractiveComponentSize()
.padding(paddingValues = padding)
.then(other = modifier),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
text = item.label,
)
}
}

View file

@ -3,10 +3,13 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
import androidx.compose.animation.AnimatedVisibility
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.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -16,8 +19,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetAction
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetActionUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetCharacteristic
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetSkill
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetSkillUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.shared.lwa.model.campaign.Campaign
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__magic_title
@ -34,9 +42,16 @@ data class CharacterDetailSheetUio(
val actions: List<CharacterDetailSheetActionUio>,
)
@Stable
object CharacterDetailSheetDefault {
@Stable
val paddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
}
@Composable
fun CharacterDetailSheet(
modifier: Modifier = Modifier,
paddings : PaddingValues = CharacterDetailSheetDefault.paddings,
sheet: State<CharacterDetailSheetUio?>,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
onSkill: (CharacterDetailSheetSkillUio) -> Unit,
@ -44,7 +59,9 @@ fun CharacterDetailSheet(
onAction: (CharacterDetailSheetActionUio) -> Unit,
) {
Row(
modifier = modifier,
modifier = modifier
.verticalScroll(state = rememberScrollState())
.padding(paddingValues = paddings),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
Column(

View file

@ -1,18 +1,22 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetActionUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetActionUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetSkillUio
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.alteration.FieldAlteration
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.usecase.ExpressionUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__con
@ -21,15 +25,6 @@ import lwacharactersheet.composeapp.generated.resources.character_sheet__charact
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__int
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__pow
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__str
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__armor
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__damage_bonus
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__hit_point
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__hp_grow
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__initiative
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__learning
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__movement
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__power_point
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__reflex
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__charisma
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__constitution
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__dexterity
@ -37,99 +32,34 @@ import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__intelligence
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__strength
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__armor
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__bonus_damage
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__hit_point
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__hp_grow
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__initiative
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__learning
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__movement
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__power_point
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__reflex
import org.jetbrains.compose.resources.getString
import java.text.Collator
class CharacterDetailFactory(
class CharacterDetailSheetFactory(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
private val expressionUseCase: ExpressionUseCase,
private val characterSheetRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
) {
suspend fun convertToCharacterDetailHeaderUio(
fun convertToCharacterDetailSheetUioFlow(
characterSheetId: String,
characterSheet: CharacterSheet?,
settings: Settings,
alterations: Map<String, List<FieldAlteration>>,
): CharacterDetailHeaderUio? {
if (characterSheet == null) return null
val alteredCharacterSheet = alteredCharacterSheetFactory.sheet(
characterSheet = characterSheet,
alterations = alterations,
)
val maxHp = alteredCharacterSheet.maxHp
val maxPp = alteredCharacterSheet.maxPp
return CharacterDetailHeaderUio(
characterSheetId = characterSheetId,
portrait = alteredCharacterSheet.portrait,
diminished = alteredCharacterSheet.diminished,
alteration = settings.isAdmin ?: false,
levelUp = alteredCharacterSheet.shouldLevelUp,
name = alteredCharacterSheet.name,
level = alteredCharacterSheet.level,
hp = "${maxHp - alteredCharacterSheet.damage}",
hpTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__hit_point),
description = getString(Res.string.tooltip__sub_characteristics__hit_point)
),
maxHp = "$maxHp",
pp = "${maxPp - alteredCharacterSheet.fatigue}",
ppTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__power_point),
description = getString(Res.string.tooltip__sub_characteristics__power_point)
),
maxPp = "$maxPp",
mov = "${alteredCharacterSheet.movement}",
movTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__movement),
description = getString(Res.string.tooltip__sub_characteristics__movement)
),
armor = "${alteredCharacterSheet.armor}",
armorTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__armor),
description = getString(Res.string.tooltip__sub_characteristics__armor)
),
bonus = alteredCharacterSheet.damageBonus,
bonusTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__damage_bonus),
description = getString(Res.string.tooltip__sub_characteristics__bonus_damage)
),
grow = "${alteredCharacterSheet.hpGrow}",
growTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__hp_grow),
description = getString(Res.string.tooltip__sub_characteristics__hp_grow)
),
learn = "${alteredCharacterSheet.learning}",
learnTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__learning),
description = getString(Res.string.tooltip__sub_characteristics__learning)
),
reflex = "${alteredCharacterSheet.reflex}",
reflexTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__reflex),
description = getString(Res.string.tooltip__sub_characteristics__reflex)
),
reflexRoll = RollAction.Uio.BoundedRollActionUio(
scope: CoroutineScope,
started: SharingStarted = SharingStarted.Eagerly,
initialValue: () -> CharacterDetailSheetUio?,
): StateFlow<CharacterDetailSheetUio?> {
return combine(
characterSheetRepository.characterDetailFlow(characterSheetId = characterSheetId),
alterationRepository.fieldAlterationsFlow(characterSheetId = characterSheetId),
) { characterSheet, alterations ->
convertToCharacterDetailSheetUio(
characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__sub_characteristics__reflex),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.reflex * 5,
),
initiative = "${alteredCharacterSheet.initiative}",
initiativeTooltip = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__initiative),
description = getString(Res.string.tooltip__sub_characteristics__initiative)
),
characterSheet = characterSheet,
alterations = alterations,
)
}.stateIn(
scope = scope,
started = started,
initialValue = initialValue(),
)
}

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable

View file

@ -105,6 +105,10 @@ abstract class CharacterRibbonViewModel(
initialValue = emptyList()
)
fun characters(index: Int): String? = characters.value
.getOrNull(index)
?.characterSheetId
@Composable
@Stable
fun roll(

View file

@ -18,7 +18,8 @@ fun NpcRibbon(
modifier: Modifier = Modifier,
viewModel: NpcRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacter: (characterSheetId: String) -> Unit,
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
@ -46,7 +47,8 @@ fun NpcRibbon(
)
CharacterPortrait(
character = it,
onCharacter = onCharacter,
onCharacterLeftClick = onCharacterLeftClick,
onCharacterRightClick = onCharacterRightClick,
onLevelUp = onLevelUp,
)
}

View file

@ -18,7 +18,8 @@ fun PlayerRibbon(
modifier: Modifier = Modifier,
viewModel: PlayerRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacter: (characterSheetId: String) -> Unit,
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
@ -37,7 +38,8 @@ fun PlayerRibbon(
) {
CharacterPortrait(
character = it,
onCharacter = onCharacter,
onCharacterLeftClick = onCharacterLeftClick,
onCharacterRightClick = onCharacterRightClick,
onLevelUp = onLevelUp,
)
CharacterPortraitRoll(

View file

@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import kotlinx.coroutines.flow.SharingStarted
@ -10,7 +11,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class CampaignToolbarViewModel(
campaignRepository: CampaignRepository,
private val characterRepository: CharacterSheetRepository,
private val campaignRepository: CampaignRepository,
networkRepository: NetworkRepository,
settingsRepository: SettingsRepository,
) : ViewModel() {

View file

@ -107,7 +107,10 @@ private fun Dialog(
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(event = onDismissRequest)
.onPreviewEscape(
escape = onDismissRequest,
enter = { onConfirm(dialog) },
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,

View file

@ -21,14 +21,11 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape
@ -93,7 +90,10 @@ private fun Dialog(
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(event = onDismissRequest)
.onPreviewEscape(
escape = onDismissRequest,
enter = { onConfirm(dialog) },
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,

View file

@ -69,7 +69,10 @@ private fun CharacterSheetDeleteConfirmationContent(
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(event = onDismissRequest)
.onPreviewEscape(
escape = onDismissRequest,
enter = { onConfirm(dialog) },
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,

View file

@ -94,7 +94,9 @@ data class GMCharacterItemUio(
}
}
@Stable
object GMCharacterPreviewDefault {
@Stable
val padding = PaddingValues(start = 16.dp)
}

View file

@ -46,7 +46,8 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.GMFilterHeader
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio
@ -62,7 +63,7 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable
fun GMCharacterPage(
viewModel: GMCharacterViewModel = koinViewModel(),
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
characterDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterSheetDiminishedDialogViewModel = koinViewModel(),
alterationViewModel: CharacterSheetAlterationDialogViewModel = koinViewModel(),
@ -84,7 +85,10 @@ fun GMCharacterPage(
onCharacterAction = viewModel::onCharacterAction,
onCharacterSheetDetail = { characterSheetId ->
scope.launch {
characterDetailViewModel.showCharacter(characterSheetId = characterSheetId)
characterDetailViewModel.showCharacter(
characterSheetId = characterSheetId,
panel = DetailPanelUio.Sheet,
)
}
},
onCharacterSheetEdit = { characterSheetId ->
@ -113,7 +117,7 @@ fun GMCharacterPage(
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
blurController = blurController,
detailViewModel = characterDetailViewModel,
detailPanelViewModel = characterDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel,

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
data class LwaShapes(
val base: Shapes,
val portrait: Shape,
val item: Shape,
val panel: Shape,
val settings: Shape,
val gameMaster: Shape,
@ -24,6 +25,7 @@ fun lwaShapes(
small = RoundedCornerShape(4.dp),
),
portrait: Shape = RoundedCornerShape(8.dp),
item: Shape = RoundedCornerShape(8.dp),
panel: Shape = RoundedCornerShape(8.dp),
settings: Shape = RoundedCornerShape(8.dp),
gameMaster: Shape = RoundedCornerShape(8.dp),
@ -31,6 +33,7 @@ fun lwaShapes(
LwaShapes(
base = base,
portrait = portrait,
item = item,
panel = panel,
settings = settings,
gameMaster = gameMaster,

View file

@ -2,16 +2,27 @@ package com.pixelized.desktop.lwa.utils.extention
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
fun Modifier.onPreviewEscape(event: () -> Unit): Modifier = this.onPreviewKeyEvent {
when {
it.key == Key.Escape -> {
event()
fun Modifier.onPreviewEscape(
escape: () -> Unit,
enter: () -> Unit,
): Modifier = this.onPreviewKeyEvent {
when (it.key) {
Key.Escape -> {
if (it.type != KeyEventType.KeyDown) escape()
true
}
Key.NumPadEnter, Key.Enter -> {
if (it.type != KeyEventType.KeyDown) enter()
true
}
Key.Spacebar -> {
true
}
else -> false
}
}

View file

@ -80,7 +80,7 @@ class InventoryStore(
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
file.name to inventory
inventory.characterSheetId to inventory
}
?.toMap()
?: emptyMap()

View file

@ -12,6 +12,7 @@ data class Inventory(
)
data class Item(
val inventoryId: String,
val itemId: String,
val count: Int,
)

View file

@ -18,6 +18,7 @@ data class InventoryJsonV1(
@Serializable
data class ItemJson(
val inventoryId: String,
val itemId: String,
val count: Int,
)

View file

@ -15,6 +15,7 @@ class InventoryJsonFactoryV1 {
),
items = json.items.map {
Inventory.Item(
inventoryId = it.inventoryId,
itemId = it.itemId,
count = it.count,
)
@ -32,6 +33,7 @@ class InventoryJsonFactoryV1 {
),
items = inventory.items.map {
InventoryJsonV1.ItemJson(
inventoryId = it.inventoryId,
itemId = it.itemId,
count = it.count,
)