diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 85dcac3..f7afba7 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -96,3 +96,7 @@ compose.desktop { } } } + +compose.resources { + generateResClass = auto +} \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_copper_32px.png b/composeApp/src/commonMain/composeResources/drawable/ic_copper_32px.png new file mode 100644 index 0000000..d5850ed Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/ic_copper_32px.png differ diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_description_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_description_24dp.xml new file mode 100644 index 0000000..7f041a7 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_description_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_gold_32px.png b/composeApp/src/commonMain/composeResources/drawable/ic_gold_32px.png new file mode 100644 index 0000000..602727b Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/ic_gold_32px.png differ diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_money_bag_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_money_bag_24dp.xml new file mode 100644 index 0000000..14f4f40 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_money_bag_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_remove_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_remove_24dp.xml new file mode 100644 index 0000000..2708e4f --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_remove_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_silver_32px.png b/composeApp/src/commonMain/composeResources/drawable/ic_silver_32px.png new file mode 100644 index 0000000..6eb1215 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/ic_silver_32px.png differ diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 1824163..b1cf9d7 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -3,6 +3,28 @@ + Écu + Écusson + Liard + + + Écu + Écu + Écus + + + + Écusson + Écusson + Écussons + + + + Liard + Liard + Liards + + La feuille de personnage est introuvable Ok @@ -162,6 +184,9 @@ Supprimer la feuille de personnage Êtes-vous sûr de vouloir supprimer "%1$s" ? + Ajouter à la bourse + Retirer de la bourse + Les caractéristiques constituent les aptitudes innées d’un 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. La Force représente essentiellement la puissance musculaire du personnage. Elle ne décrit pas nécessairement la masse musculaire brute, mais l’efficacité 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 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 qu’un personnage peut supporter avant de succomber.\n\n- Point de vie maximum\n- Athlétisme\n- Acrobatie diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt index c863b82..2473584 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 57bff04..520c14d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index 2e75a8a..6d70e5d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -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 + // Inventory + + suspend fun getInventory( + characterSheetId: String, + ): APIResponse + + suspend fun putInventory( + json: InventoryJson, + create: Boolean, + ): APIResponse + + suspend fun deleteInventory( + characterSheetId: String, + ): APIResponse + // Tags suspend fun getAlterationTags(): APIResponse> diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index 26769f8..0e878bf 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -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 = client + .get("$root/inventory/detail?characterSheetId=$characterSheetId") + .body() + + @Throws + override suspend fun putInventory( + inventory: InventoryJson, + create: Boolean, + ): APIResponse = client + .put("$root/inventory/update?create=$create") { + contentType(ContentType.Application.Json) + setBody(inventory) + } + .body>() + + @Throws + override suspend fun deleteInventory(characterSheetId: String): APIResponse = client + .delete("$root/inventory/delete?characterSheetId=$characterSheetId") + .body() + @Throws override suspend fun getAlterationTags(): APIResponse> = client .get("$root/tag/alteration") diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/inventory/InventoryRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/inventory/InventoryRepository.kt new file mode 100644 index 0000000..d6f0db2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/inventory/InventoryRepository.kt @@ -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 { + 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, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/inventory/InventoryStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/inventory/InventoryStore.kt new file mode 100644 index 0000000..763c243 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/inventory/InventoryStore.kt @@ -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>(emptyMap()) + val inventories: StateFlow> = _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 +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt index 601d46a..e4c0dbe 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt index 4e1c2f8..9ca3d7b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialog.kt new file mode 100644 index 0000000..c38c47c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialog.kt @@ -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, + val add: StateFlow, + val gold: LwaTextFieldUio, + val silver: LwaTextFieldUio, + val copper: LwaTextFieldUio, + val enableConfirm: StateFlow, +) + +@Composable +fun PurseDialog( + dialog: State, + 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, + 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 + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialogFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialogFactory.kt new file mode 100644 index 0000000..a6195e9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialogFactory.kt @@ -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, + ): 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>.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> { + return MutableStateFlow(initialValue) to MutableStateFlow(initialError) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialogViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialogViewModel.kt new file mode 100644 index 0000000..55360c2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialogViewModel.kt @@ -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(null) + private val signFlow = MutableStateFlow(true) + + private val _error = MutableSharedFlow() + val error: MutableSharedFlow = _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 + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index 12ad9ed..0fafa77 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -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 { @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 } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortrait.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortrait.kt index 6c28992..5da5b21 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortrait.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortrait.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailPanel.kt similarity index 71% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailPanel.kt index 25d0614..ccf2552 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailPanel.kt @@ -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, val sheet: StateFlow, + val inventory: StateFlow, ) +@Stable +enum class DetailPanelUio { + Sheet, + Inventory, +} + @Composable fun CharacterDetailPanel( modifier: Modifier = Modifier, blurController: BlurContentController, transitionSpec: AnimatedContentTransitionScope.() -> 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 = detailViewModel.detail.collectAsState() + val pager = rememberPagerState(initialPage = 0) { DetailPanelUio.entries.size } + val detail: State = 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, transitionSpec: AnimatedContentTransitionScope.() -> 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, - sheet: State, + 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(), + ) + } + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailPanelViewModel.kt similarity index 58% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailPanelViewModel.kt index 86560c6..f1eb5b8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailPanelViewModel.kt @@ -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(null) + private val characterSheetPanelFlow = MutableStateFlow(null) - val detail: StateFlow = displayedCharacterId - .map { characterSheetId -> - if (characterSheetId == null) return@map empty() - - CharacterDetailPanelUio( + val detail: StateFlow = 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, ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt index b428d95..f88bbae 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt @@ -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, 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( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeaderFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeaderFactory.kt new file mode 100644 index 0000000..157651d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeaderFactory.kt @@ -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 { + 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>, + ): 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) + ), + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventory.kt new file mode 100644 index 0000000..c865368 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventory.kt @@ -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, +) { + @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, +) { + 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.() -> ContentTransform = { + val enter = fadeIn() + slideInVertically { -16 } + val exit = fadeOut() + slideOutVertically { 16 } + enter togetherWith exit using SizeTransform(clip = false) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventoryFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventoryFactory.kt new file mode 100644 index 0000000..8bfdc8b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventoryFactory.kt @@ -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 { + 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?, + items: Map, + ): 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() + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryItem.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryItem.kt new file mode 100644 index 0000000..3ce21f0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryItem.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt index 1373241..ae4c846 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt @@ -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, ) +@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, 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( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetFactory.kt similarity index 69% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetFactory.kt index 70e976e..236f194 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetFactory.kt @@ -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>, - ): 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 { + 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(), ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetAction.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetAction.kt index 831d86d..4b24f4f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetAction.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetCharacteristic.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetCharacteristic.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetCharacteristic.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetCharacteristic.kt index 6e408ab..549857f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetCharacteristic.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetCharacteristic.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetSkill.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetSkill.kt index adb72f9..edc07e4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/item/CharacterDetailSheetSkill.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt index 155ba74..7db88c2 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/CharacterRibbonViewModel.kt @@ -105,6 +105,10 @@ abstract class CharacterRibbonViewModel( initialValue = emptyList() ) + fun characters(index: Int): String? = characters.value + .getOrNull(index) + ?.characterSheetId + @Composable @Stable fun roll( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbon.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbon.kt index 6ed1ab5..371fcb9 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbon.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbon.kt @@ -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, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbon.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbon.kt index c927a97..f409f08 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbon.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbon.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt index df5ac2c..96d0798 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt @@ -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() { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/copy/CharacterSheetCopyDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/copy/CharacterSheetCopyDialog.kt index d5c5b01..ffadf4b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/copy/CharacterSheetCopyDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/copy/CharacterSheetCopyDialog.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/delete/CharacterSheetDeleteDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/delete/CharacterSheetDeleteDialog.kt index 03a27ec..ed90852 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/delete/CharacterSheetDeleteDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/delete/CharacterSheetDeleteDialog.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/CharacterSheetDeleteConfirmationDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/CharacterSheetDeleteConfirmationDialog.kt index 73138d5..e7e890b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/CharacterSheetDeleteConfirmationDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/CharacterSheetDeleteConfirmationDialog.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacter.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacter.kt index ea79403..0f2301f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacter.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacter.kt @@ -94,7 +94,9 @@ data class GMCharacterItemUio( } } +@Stable object GMCharacterPreviewDefault { + @Stable val padding = PaddingValues(start = 16.dp) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt index eb9a50f..e20a0bf 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt index 97b11a7..95f8ce7 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+KeyEvent.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+KeyEvent.kt index 852898f..3be9c89 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+KeyEvent.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+KeyEvent.kt @@ -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 } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt index 82d9d1c..3352a67 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt @@ -80,7 +80,7 @@ class InventoryStore( } catch (exception: Exception) { throw JsonConversionException(root = exception) } - file.name to inventory + inventory.characterSheetId to inventory } ?.toMap() ?: emptyMap() diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/Inventory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/Inventory.kt index 4a01ace..3d08efe 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/Inventory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/Inventory.kt @@ -12,6 +12,7 @@ data class Inventory( ) data class Item( + val inventoryId: String, val itemId: String, val count: Int, ) diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/InventoryJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/InventoryJsonV1.kt index 2782e94..990a5b6 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/InventoryJsonV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/InventoryJsonV1.kt @@ -18,6 +18,7 @@ data class InventoryJsonV1( @Serializable data class ItemJson( + val inventoryId: String, val itemId: String, val count: Int, ) diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/factory/InventoryJsonFactoryV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/factory/InventoryJsonFactoryV1.kt index 994f9f9..ced7139 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/factory/InventoryJsonFactoryV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/inventory/factory/InventoryJsonFactoryV1.kt @@ -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, )