Add confirmation dialog for GM actions.

This commit is contained in:
Thomas Andres Gomez 2025-10-12 17:48:11 +02:00
parent 9be8f2b209
commit 651d05a7c4
13 changed files with 315 additions and 88 deletions

View file

@ -329,5 +329,17 @@
<string name="game_master__item__edit_consumable">Consommable</string> <string name="game_master__item__edit_consumable">Consommable</string>
<string name="game_master__item__edit_add_alteration">Ajouter une alteration</string> <string name="game_master__item__edit_add_alteration">Ajouter une alteration</string>
<string name="game_master__character_edit__title">Édition de personnage</string> <string name="game_master__character_edit__title">Édition de personnage</string>
<string name="game_master__actions__on_server_sync__title">Synchronisation du serveur</string>
<string name="game_master__actions__on_server_sync__description">Demander au serveur d'invalider son cache</string>
<string name="game_master__actions__party_heal__title">Soigner les personnages joueurs</string>
<string name="game_master__actions__party_heal__description">Cette action réinitialisera les points de vie, de pouvoir et d'état diminué de chaque personnage joueur présent dans le groupe.</string>
<string name="game_master__actions__hide_player__title">Cacher le groupe de personnages joueur</string>
<string name="game_master__actions__hide_player__description">Cacher le panneau latéral gauche pour tous les joueurs.</string>
<string name="game_master__actions__show_player__title">Montrer les personnages joueurs</string>
<string name="game_master__actions__show_player__description">Montrer le panneau latéral gauche pour tous les joueurs.</string>
<string name="game_master__actions__hide_npc__title">Cacher le groupe de npcs</string>
<string name="game_master__actions__hide_npc__description">Cacher le panneau latéral droit pour tous les joueurs.</string>
<string name="game_master__actions__show_npc__title">Montrer le groupe de npcs</string>
<string name="game_master__actions__show_npc__description">Montrer le panneau latéral droit pour tous les joueurs.</string>
</resources> </resources>

View file

@ -47,6 +47,7 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.text.TextMessageFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.links.ResourcesViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.links.ResourcesViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionUseCase
import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditFactory import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditViewModel
@ -198,4 +199,5 @@ val viewModelDependencies
val useCaseDependencies val useCaseDependencies
get() = module { get() = module {
factoryOf(::SettingsUseCase) factoryOf(::SettingsUseCase)
factoryOf(::GMActionUseCase)
} }

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
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.DisposableEffect
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
@ -18,6 +19,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
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
@ -29,6 +32,7 @@ object LwaDialogDefault {
@Composable @Composable
fun <T> LwaDialog( fun <T> LwaDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
blur: BlurContentController? = LocalBlurController.current,
paddings: PaddingValues = LwaDialogDefault.paddings, paddings: PaddingValues = LwaDialogDefault.paddings,
color: Color = MaterialTheme.colors.surface, color: Color = MaterialTheme.colors.surface,
state: State<T?>, state: State<T?>,
@ -37,6 +41,16 @@ fun <T> LwaDialog(
content: @Composable BoxScope.(T) -> Unit, content: @Composable BoxScope.(T) -> Unit,
) { ) {
state.value?.let { dialog -> state.value?.let { dialog ->
blur?.let {
DisposableEffect("LwaDialog") {
blur.show()
onDispose {
blur.hide()
}
}
}
Dialog( Dialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
content = { content = {

View file

@ -19,6 +19,7 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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
@ -28,13 +29,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
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
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
@ -52,12 +52,23 @@ data class CharacterSheetAlterationDialogUio(
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun CharacterSheetAlterationDialog( fun CharacterSheetAlterationDialog(
blur: BlurContentController? = LocalBlurController.current,
dialog: State<CharacterSheetAlterationDialogUio?>, dialog: State<CharacterSheetAlterationDialogUio?>,
onTag: (String) -> Unit, onTag: (String) -> Unit,
onAlteration: (characterSheetId: String, alterationId: String, active: Boolean) -> Unit, onAlteration: (characterSheetId: String, alterationId: String, active: Boolean) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
dialog.value?.let { dialog.value?.let {
blur?.let {
DisposableEffect("LwaDialog") {
blur.show()
onDispose {
blur.hide()
}
}
}
Dialog( Dialog(
properties = DialogProperties( properties = DialogProperties(
usePlatformDefaultWidth = false, usePlatformDefaultWidth = false,

View file

@ -0,0 +1,98 @@
package com.pixelized.desktop.lwa.ui.composable.confirmation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action
import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action
import org.jetbrains.compose.resources.stringResource
@Stable
data class ConfirmationDialogUio(
val title: String,
val description: String,
val onConfirmRequest: () -> Unit,
val onDismissRequest: () -> Unit,
)
@Stable
object ConfirmationDialogDefault {
@Stable
val paddings = PaddingValues(start = 16.dp, top = 16.dp, end = 16.dp)
@Stable
val spacings: Dp = 8.dp
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ConfirmationDialog(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = ConfirmationDialogDefault.paddings,
spacing: Dp = ConfirmationDialogDefault.spacings,
dialog: State<ConfirmationDialogUio?>,
) {
LwaDialog(
modifier = modifier,
blur = LocalBlurController.current,
state = dialog,
onDismissRequest = { dialog.value?.onDismissRequest?.invoke() },
onConfirm = { dialog.value?.onConfirmRequest?.invoke() },
) {
Column(
modifier = Modifier.padding(paddingValues = paddingValues),
verticalArrangement = Arrangement.spacedBy(space = spacing),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
style = MaterialTheme.typography.caption,
text = it.title,
)
Text(
style = MaterialTheme.typography.body1,
text = it.description,
)
Row(
modifier = Modifier.align(alignment = Alignment.End),
horizontalArrangement = Arrangement.spacedBy(
space = spacing / 2,
alignment = Alignment.End,
),
) {
TextButton(
onClick = it.onDismissRequest,
) {
Text(
color = MaterialTheme.colors.primaryVariant,
text = stringResource(Res.string.dialog__cancel_action)
)
}
TextButton(
onClick = it.onConfirmRequest,
) {
Text(
color = MaterialTheme.colors.primary,
text = stringResource(Res.string.dialog__confirm_action)
)
}
}
}
}
}

View file

@ -169,7 +169,6 @@ fun CampaignScreen(
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight(), .fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr), transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr),
blurController = blurController,
detailPanelViewModel = npcDetailViewModel, detailPanelViewModel = npcDetailViewModel,
characterDiminishedViewModel = dismissedViewModel, characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel, characteristicDialogViewModel = characteristicDialogViewModel,
@ -186,7 +185,6 @@ fun CampaignScreen(
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight(), .fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl), transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
blurController = blurController,
detailPanelViewModel = playerDetailViewModel, detailPanelViewModel = playerDetailViewModel,
characterDiminishedViewModel = dismissedViewModel, characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel, characteristicDialogViewModel = characteristicDialogViewModel,

View file

@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
@ -31,7 +30,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalRollHostState import com.pixelized.desktop.lwa.LocalRollHostState
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.character.alterteration.CharacterSheetAlterationDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.alterteration.CharacterSheetAlterationDialogViewModel
@ -74,7 +72,6 @@ enum class DetailPanelUio {
@Composable @Composable
fun CharacterDetailPanel( fun CharacterDetailPanel(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
blurController: BlurContentController,
transitionSpec: AnimatedContentTransitionScope<CharacterDetailPanelUio>.() -> ContentTransform = rememberTransitionAnimation(), transitionSpec: AnimatedContentTransitionScope<CharacterDetailPanelUio>.() -> ContentTransform = rememberTransitionAnimation(),
detailPanelViewModel: CharacterDetailPanelViewModel, detailPanelViewModel: CharacterDetailPanelViewModel,
characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel, characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel,
@ -121,12 +118,10 @@ fun CharacterDetailPanel(
} }
}, },
onAlteration = { onAlteration = {
blurController.show()
alterationViewModel.show(characterSheetId = it) alterationViewModel.show(characterSheetId = it)
}, },
onDiminished = { onDiminished = {
scope.launch { scope.launch {
blurController.show()
characterDiminishedViewModel.showDiminishedDialog( characterDiminishedViewModel.showDiminishedDialog(
characterSheetId = it characterSheetId = it
) )
@ -134,7 +129,6 @@ fun CharacterDetailPanel(
}, },
onHp = { onHp = {
scope.launch { scope.launch {
blurController.show()
characteristicDialogViewModel.showSubCharacteristicDialog( characteristicDialogViewModel.showSubCharacteristicDialog(
characterSheetId = it, characterSheetId = it,
characteristic = CharacterSheetCharacteristicDialogUio.Characteristic.Damage, characteristic = CharacterSheetCharacteristicDialogUio.Characteristic.Damage,
@ -143,7 +137,6 @@ fun CharacterDetailPanel(
}, },
onPp = { onPp = {
scope.launch { scope.launch {
blurController.show()
characteristicDialogViewModel.showSubCharacteristicDialog( characteristicDialogViewModel.showSubCharacteristicDialog(
characterSheetId = it, characterSheetId = it,
characteristic = CharacterSheetCharacteristicDialogUio.Characteristic.Fatigue, characteristic = CharacterSheetCharacteristicDialogUio.Characteristic.Fatigue,

View file

@ -107,7 +107,6 @@ fun CharacterDetailInventory(
itemDetailDialogViewModel: ItemDetailDialogViewModel = koinViewModel(), itemDetailDialogViewModel: ItemDetailDialogViewModel = koinViewModel(),
inventory: State<CharacterDetailInventoryUio?>, inventory: State<CharacterDetailInventoryUio?>,
) { ) {
val blur = LocalBlurController.current
val focus = LocalFocusManager.current val focus = LocalFocusManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -120,14 +119,12 @@ fun CharacterDetailInventory(
spacing = spacing, spacing = spacing,
inventory = unWrap, inventory = unWrap,
onPurse = { onPurse = {
blur.show()
purseViewModel.showPurseDialog( purseViewModel.showPurseDialog(
characterSheetId = it, characterSheetId = it,
) )
focus.clearFocus(force = true) focus.clearFocus(force = true)
}, },
onItem = { item -> onItem = { item ->
blur.show()
itemDetailDialogViewModel.showItemDialog( itemDetailDialogViewModel.showItemDialog(
characterSheetId = item.characterSheetId, characterSheetId = item.characterSheetId,
inventoryId = item.inventoryId, inventoryId = item.inventoryId,
@ -136,7 +133,6 @@ fun CharacterDetailInventory(
focus.clearFocus(force = true) focus.clearFocus(force = true)
}, },
onAddItem = { onAddItem = {
blur.show()
inventoryDialogViewModel.showInventoryDialog( inventoryDialogViewModel.showInventoryDialog(
characterSheetId = it, characterSheetId = it,
) )
@ -166,7 +162,6 @@ fun CharacterDetailInventory(
PurseDialog( PurseDialog(
dialog = purseViewModel.purseDialog.collectAsState(), dialog = purseViewModel.purseDialog.collectAsState(),
onDismissRequest = { onDismissRequest = {
blur.hide()
purseViewModel.hidePurseDialog() purseViewModel.hidePurseDialog()
}, },
onSwapSign = { onSwapSign = {
@ -175,7 +170,6 @@ fun CharacterDetailInventory(
onConfirm = { onConfirm = {
scope.launch { scope.launch {
if (purseViewModel.confirmPurse(dialog = it)) { if (purseViewModel.confirmPurse(dialog = it)) {
blur.hide()
purseViewModel.hidePurseDialog() purseViewModel.hidePurseDialog()
} }
} }
@ -185,11 +179,9 @@ fun CharacterDetailInventory(
InventoryDialog( InventoryDialog(
dialog = inventoryDialogViewModel.inventoryDialog.collectAsState(), dialog = inventoryDialogViewModel.inventoryDialog.collectAsState(),
onDismissRequest = { onDismissRequest = {
blur.hide()
inventoryDialogViewModel.hideInventoryDialog() inventoryDialogViewModel.hideInventoryDialog()
}, },
onItem = { dialog, itemId -> onItem = { dialog, itemId ->
blur.show()
itemDetailDialogViewModel.showItemDialog( itemDetailDialogViewModel.showItemDialog(
characterSheetId = dialog.characterSheetId, characterSheetId = dialog.characterSheetId,
inventoryId = null, inventoryId = null,
@ -201,7 +193,6 @@ fun CharacterDetailInventory(
ItemDetailDialog( ItemDetailDialog(
dialog = itemDetailDialogViewModel.itemDialog.collectAsState(), dialog = itemDetailDialogViewModel.itemDialog.collectAsState(),
onDismissRequest = { onDismissRequest = {
blur.hide()
itemDetailDialogViewModel.hideItemDialog() itemDetailDialogViewModel.hideItemDialog()
}, },
onAddItem = { dialog -> onAddItem = { dialog ->
@ -210,7 +201,6 @@ fun CharacterDetailInventory(
dialog = dialog, dialog = dialog,
) )
if (result) { if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog() itemDetailDialogViewModel.hideItemDialog()
} }
} }
@ -221,7 +211,6 @@ fun CharacterDetailInventory(
dialog = dialog, dialog = dialog,
) )
if (result) { if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog() itemDetailDialogViewModel.hideItemDialog()
} }
} }
@ -233,7 +222,6 @@ fun CharacterDetailInventory(
inventoryId = dialog.inventoryId, inventoryId = dialog.inventoryId,
) )
if (result) { if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog() itemDetailDialogViewModel.hideItemDialog()
} }
} }
@ -245,7 +233,6 @@ fun CharacterDetailInventory(
inventoryId = dialog.inventoryId, inventoryId = dialog.inventoryId,
) )
if (result) { if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog() itemDetailDialogViewModel.hideItemDialog()
} }
} }
@ -257,7 +244,6 @@ fun CharacterDetailInventory(
inventoryId = dialog.inventoryId, inventoryId = dialog.inventoryId,
) )
if (result) { if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog() itemDetailDialogViewModel.hideItemDialog()
} }
} }

View file

@ -10,12 +10,13 @@ import androidx.compose.foundation.verticalScroll
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.collectAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import com.pixelized.desktop.lwa.ui.composable.confirmation.ConfirmationDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_camping_24dp import lwacharactersheet.composeapp.generated.resources.ic_camping_24dp
@ -36,7 +37,8 @@ fun GMActionPage(
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scroll = rememberScrollState() val scroll = rememberScrollState()
val actions = viewModel.actions.collectAsState() val actions = viewModel.actions.collectAsStateWithLifecycle()
val validationDialog = viewModel.validationDialog.collectAsStateWithLifecycle()
GMActionContent( GMActionContent(
actions = actions, actions = actions,
@ -66,6 +68,10 @@ fun GMActionPage(
ErrorSnackHandler( ErrorSnackHandler(
error = viewModel.error, error = viewModel.error,
) )
ConfirmationDialog(
dialog = validationDialog,
)
} }
@Composable @Composable

View file

@ -0,0 +1,58 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.action
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.shared.lwa.protocol.websocket.GameAdminEvent
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
class GMActionUseCase(
private val characterRepository: CharacterSheetRepository,
private val networkRepository: NetworkRepository,
private val campaignRepository: CampaignRepository,
) {
suspend fun invalidateServerCache() {
networkRepository.share(
GameAdminEvent.ServerSynchronization(
timestamp = System.currentTimeMillis(),
)
)
}
suspend fun healPlayerParty() {
campaignRepository.campaignFlow().value.characters.forEach { characterSheetId ->
val sheet = characterRepository.characterDetail(
characterSheetId = characterSheetId,
) ?: return@forEach
val updated = sheet.copy(
damage = 0,
fatigue = 0,
diminished = 0,
)
if (sheet != updated) {
characterRepository.updateCharacter(
sheet = updated,
create = false,
)
}
}
}
suspend fun toggleNpcVisibility() {
networkRepository.share(
GameMasterEvent.ToggleNpc(
timestamp = System.currentTimeMillis(),
)
)
}
suspend fun togglePlayerVisibility() {
networkRepository.share(
GameMasterEvent.TogglePlayer(
timestamp = System.currentTimeMillis(),
)
)
}
}

View file

@ -3,23 +3,36 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster.action
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.ui.composable.confirmation.ConfirmationDialogUio
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import com.pixelized.shared.lwa.protocol.websocket.GameAdminEvent
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__actions__hide_npc__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__hide_npc__title
import lwacharactersheet.composeapp.generated.resources.game_master__actions__hide_player__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__hide_player__title
import lwacharactersheet.composeapp.generated.resources.game_master__actions__on_server_sync__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__on_server_sync__title
import lwacharactersheet.composeapp.generated.resources.game_master__actions__party_heal__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__party_heal__title
import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_npc__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_npc__title
import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_player__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_player__title
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
class GMActionViewModel( class GMActionViewModel(
private val characterRepository: CharacterSheetRepository, private val actionUseCase: GMActionUseCase,
private val networkRepository: NetworkRepository, campaignRepository: CampaignRepository,
private val campaignRepository: CampaignRepository,
) : ViewModel() { ) : ViewModel() {
private val _error = MutableSharedFlow<ErrorSnackUio>() private val _error = MutableSharedFlow<ErrorSnackUio>()
@ -39,68 +52,102 @@ class GMActionViewModel(
initialValue = null, initialValue = null,
) )
private val _validationDialog = MutableStateFlow<ConfirmationDialogUio?>(null)
val validationDialog: StateFlow<ConfirmationDialogUio?> = _validationDialog
suspend fun onServerSync() { suspend fun onServerSync() {
try { showConfirmationDialog(
networkRepository.share( title = Res.string.game_master__actions__on_server_sync__title,
GameAdminEvent.ServerSynchronization( description = Res.string.game_master__actions__on_server_sync__description,
timestamp = System.currentTimeMillis(), onConfirmationRequest = {
)
)
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
}
}
suspend fun onPartyHeal() {
campaignRepository.campaignFlow().value.characters.forEach { characterSheetId ->
val sheet = characterRepository.characterDetail(
characterSheetId = characterSheetId,
) ?: return@forEach
val updated = sheet.copy(
damage = 0,
fatigue = 0,
diminished = 0,
)
if (sheet != updated) {
try { try {
characterRepository.updateCharacter( actionUseCase.invalidateServerCache()
sheet = updated,
create = false,
)
} catch (exception: Exception) { } catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception) val message = ErrorSnackUio.from(exception = exception)
_error.emit(message) _error.emit(message)
} }
},
onDismissRequest = {
_validationDialog.value = null
} }
} )
} }
suspend fun onNpcVisibility() { suspend fun onPartyHeal() {
try { showConfirmationDialog(
networkRepository.share( title = Res.string.game_master__actions__party_heal__title,
GameMasterEvent.ToggleNpc( description = Res.string.game_master__actions__party_heal__description,
timestamp = System.currentTimeMillis(), onConfirmationRequest = {
) try {
) actionUseCase.healPlayerParty()
} catch (exception: Exception) { } catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception) val message = ErrorSnackUio.from(exception = exception)
_error.emit(message) _error.emit(message)
} }
},
)
} }
suspend fun onPlayerVisibility() { suspend fun onPlayerVisibility() {
try { showConfirmationDialog(
networkRepository.share( title = when (actions.value?.party) {
GameMasterEvent.TogglePlayer( true -> Res.string.game_master__actions__hide_player__title
timestamp = System.currentTimeMillis(), else -> Res.string.game_master__actions__show_player__title
) },
) description = when (actions.value?.party) {
} catch (exception: Exception) { true -> Res.string.game_master__actions__hide_player__description
val message = ErrorSnackUio.from(exception = exception) else -> Res.string.game_master__actions__show_player__description
_error.emit(message) },
} onConfirmationRequest = {
try {
actionUseCase.togglePlayerVisibility()
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
}
},
)
}
suspend fun onNpcVisibility() {
showConfirmationDialog(
title = when (actions.value?.npc) {
true -> Res.string.game_master__actions__hide_npc__title
else -> Res.string.game_master__actions__show_npc__title
},
description = when (actions.value?.npc) {
true -> Res.string.game_master__actions__hide_npc__description
else -> Res.string.game_master__actions__show_npc__description
},
onConfirmationRequest = {
try {
actionUseCase.toggleNpcVisibility()
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
}
},
)
}
private suspend inline fun showConfirmationDialog(
title: StringResource,
description: StringResource,
crossinline onConfirmationRequest: suspend () -> Unit,
crossinline onDismissRequest: () -> Unit = { _validationDialog.value = null },
) {
_validationDialog.value = ConfirmationDialogUio(
title = getString(title),
description = getString(description),
onConfirmRequest = {
viewModelScope.launch {
onConfirmationRequest()
onDismissRequest()
}
},
onDismissRequest = {
onDismissRequest()
},
)
} }
} }

View file

@ -109,7 +109,6 @@ fun GMCharacterPage(
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight(), .fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl), transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
blurController = blurController,
detailPanelViewModel = characterDetailViewModel, detailPanelViewModel = characterDetailViewModel,
characterDiminishedViewModel = dismissedViewModel, characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel, characteristicDialogViewModel = characteristicDialogViewModel,

View file

@ -55,7 +55,10 @@ data class LwaColors(
@Composable @Composable
@Stable @Stable
fun darkLwaColorTheme( fun darkLwaColorTheme(
base: Colors = darkColors(), base: Colors = darkColors(
primary = Color(0xFFBB86FC),
primaryVariant = Color(0xB2BB86FC),
),
elevated: LwaColors.Elevated = LwaColors.Elevated( elevated: LwaColors.Elevated = LwaColors.Elevated(
base1dp = base.calculateElevatedColor( base1dp = base.calculateElevatedColor(
color = base.surface, color = base.surface,