Add client purse management.

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,8 +13,11 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -26,9 +29,11 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction 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 com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__level 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_blur_on_24dp
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import lwacharactersheet.composeapp.generated.resources.ic_cognition_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_directions_run_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_plus_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_pan_tool_24dp
import lwacharactersheet.composeapp.generated.resources.ic_shield_24dp import lwacharactersheet.composeapp.generated.resources.ic_shield_24dp
import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp
@ -59,6 +67,7 @@ import org.jetbrains.compose.resources.stringResource
@Stable @Stable
data class CharacterDetailHeaderUio( data class CharacterDetailHeaderUio(
val characterSheetId: String, val characterSheetId: String,
val panel: DetailPanelUio,
val portrait: String?, val portrait: String?,
val diminished: Int, val diminished: Int,
val alteration: Boolean, val alteration: Boolean,
@ -88,25 +97,39 @@ data class CharacterDetailHeaderUio(
val initiativeTooltip: TooltipUio, val initiativeTooltip: TooltipUio,
) )
@Stable
object CharacterDetailHeaderDefault {
@Stable
val paddings = PaddingValues(start = 16.dp, end = 16.dp, bottom = 8.dp)
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun CharacterDetailHeader( fun CharacterDetailHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
paddings: PaddingValues = CharacterDetailHeaderDefault.paddings,
iconSize: Dp = MaterialTheme.lwa.size.sheet.subCategory, iconSize: Dp = MaterialTheme.lwa.size.sheet.subCategory,
header: State<CharacterDetailHeaderUio?>, header: State<CharacterDetailHeaderUio?>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onLevelUp: () -> Unit, onLevelUp: () -> Unit,
onSheet: () -> Unit,
onInventory: () -> Unit,
onAlteration: () -> Unit, onAlteration: () -> Unit,
onDiminished: () -> Unit, onDiminished: () -> Unit,
onHp: () -> Unit, onHp: () -> Unit,
onPp: () -> Unit, onPp: () -> Unit,
onReflex: (RollAction.Uio) -> 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( Column(
modifier = modifier, modifier = modifier,
) { ) {
Row( Row(
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = startPadding),
) { ) {
Row( Row(
modifier = Modifier 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( AnimatedVisibility(
visible = header.value?.alteration == true, visible = header.value?.alteration == true,
enter = fadeIn(), 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 { Box {
IconButton( IconButton(
onClick = onDiminished, onClick = onDiminished,
@ -200,7 +254,11 @@ fun CharacterDetailHeader(
} }
} }
Row( Row(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(
start = startPadding,
end = endPadding,
bottom = bottomPadding,
),
horizontalArrangement = Arrangement.spacedBy(space = 10.dp), horizontalArrangement = Arrangement.spacedBy(space = 10.dp),
) { ) {
TooltipLayout( TooltipLayout(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,22 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetActionUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetActionUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.item.CharacterDetailSheetSkillUio
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.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.alteration.FieldAlteration
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.usecase.ExpressionUseCase 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.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__con 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__int
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__pow 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__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__charisma
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__constitution import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__constitution
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__dexterity 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__intelligence
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__strength 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 org.jetbrains.compose.resources.getString
import java.text.Collator import java.text.Collator
class CharacterDetailFactory( class CharacterDetailSheetFactory(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
private val expressionUseCase: ExpressionUseCase, private val expressionUseCase: ExpressionUseCase,
private val characterSheetRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
) { ) {
suspend fun convertToCharacterDetailHeaderUio( fun convertToCharacterDetailSheetUioFlow(
characterSheetId: String, characterSheetId: String,
characterSheet: CharacterSheet?, scope: CoroutineScope,
settings: Settings, started: SharingStarted = SharingStarted.Eagerly,
alterations: Map<String, List<FieldAlteration>>, initialValue: () -> CharacterDetailSheetUio?,
): CharacterDetailHeaderUio? { ): StateFlow<CharacterDetailSheetUio?> {
if (characterSheet == null) return null return combine(
characterSheetRepository.characterDetailFlow(characterSheetId = characterSheetId),
val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( alterationRepository.fieldAlterationsFlow(characterSheetId = characterSheetId),
characterSheet = characterSheet, ) { characterSheet, alterations ->
alterations = alterations, convertToCharacterDetailSheetUio(
)
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(
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
label = getString(Res.string.character_sheet__sub_characteristics__reflex), characterSheet = characterSheet,
rollAction = "1d100", alterations = alterations,
rollSuccessValue = alteredCharacterSheet.reflex * 5, )
), }.stateIn(
initiative = "${alteredCharacterSheet.initiative}", scope = scope,
initiativeTooltip = TooltipUio( started = started,
title = getString(Res.string.character_sheet__sub_characteristics__initiative), initialValue = initialValue(),
description = getString(Res.string.tooltip__sub_characteristics__initiative)
),
) )
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,16 +2,27 @@ package com.pixelized.desktop.lwa.utils.extention
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key 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.key
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
fun Modifier.onPreviewEscape(event: () -> Unit): Modifier = this.onPreviewKeyEvent { fun Modifier.onPreviewEscape(
when { escape: () -> Unit,
it.key == Key.Escape -> { enter: () -> Unit,
event() ): 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 true
} }
else -> false else -> false
} }
} }

View file

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

View file

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

View file

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

View file

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