diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_edit_note_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_edit_note_24dp.xml new file mode 100644 index 0000000..6bdce88 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_edit_note_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index dcb2884..393fff4 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -325,6 +325,7 @@ Édition de personnage Synchronisation du serveur Demander au serveur d'invalider son cache + Titre de la scène Soigner les personnages joueurs Cette action réinitialisera les points de vie, de pouvoir et d'état diminué de chaque personnage joueur présent dans le groupe. Cacher le groupe de personnages joueur diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index 84d8ceb..b595475 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -33,6 +33,10 @@ interface LwaClient { suspend fun getCampaign(): APIResponse + suspend fun putCampaignScene( + scene: CampaignJson.SceneJson, + ): APIResponse + suspend fun putCampaignCharacter( characterSheetId: String, ): APIResponse diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index 28f3b41..44b73cc 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -58,6 +58,16 @@ class LwaClientImpl( .get("$root/campaign") .body() + @Throws + override suspend fun putCampaignScene( + scene: CampaignJson.SceneJson, + ): APIResponse = client + .put("$root/campaign/scene") { + contentType(ContentType.Application.Json) + setBody(scene) + } + .body>() + @Throws override suspend fun putCampaignCharacter( characterSheetId: String, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt index b61a3f2..d242d9c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt @@ -14,6 +14,12 @@ class CampaignRepository( store.updateCampaignFlow() } + suspend fun updateSceneTitle( + title: String, + ) { + store.changeSceneTitle(title = title) + } + @Throws suspend fun addCharacter( characterSheetId: String, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt index 402211f..3693f8a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt @@ -40,6 +40,17 @@ class CampaignStore( } } + @Throws + suspend fun changeSceneTitle( + title: String, + ) { + val scene = factory.createScene(title = title) + val request = client.putCampaignScene(scene = scene) + if (request.success.not()) { + LwaClient.error(error = request) + } + } + @Throws suspend fun addCharacter( characterSheetId: String, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonAlteration.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonAlteration.kt index 5da895b..000d761 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonAlteration.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonAlteration.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import coil3.PlatformContext @@ -30,10 +29,10 @@ import coil3.request.ImageRequest import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipLayout import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipUio import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.utils.extention.invert @Stable data class CharacterRibbonAlterationUio( - val icon: String, val tooltips: BasicTooltipUio?, ) @@ -63,14 +62,10 @@ fun CharacterRibbonAlteration( delayMillis = 0, tooltip = it.tooltips, tooltipPlacement = remember(direction) { - TooltipPlacement.ComponentRect( - anchor = when (direction) { - LayoutDirection.Ltr -> Alignment.CenterEnd - LayoutDirection.Rtl -> Alignment.CenterStart - }, - alignment = when (direction) { - LayoutDirection.Ltr -> Alignment.CenterEnd - LayoutDirection.Rtl -> Alignment.CenterStart + TooltipPlacement.CursorPoint( + alignment = when (direction.invert) { + LayoutDirection.Ltr -> Alignment.CenterStart + LayoutDirection.Rtl -> Alignment.CenterEnd }, ) }, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt index 74860c8..4b824fd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State @@ -15,11 +16,13 @@ import androidx.compose.ui.Modifier 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.confirmation.ConfirmationDialog +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler +import com.pixelized.desktop.lwa.ui.theme.lwa import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_camping_24dp +import lwacharactersheet.composeapp.generated.resources.ic_edit_note_24dp import lwacharactersheet.composeapp.generated.resources.ic_sync_24dp import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp @@ -38,6 +41,7 @@ fun GMActionPage( val scope = rememberCoroutineScope() val scroll = rememberScrollState() val actions = viewModel.actions.collectAsStateWithLifecycle() + val editDialog = viewModel.editDialog.collectAsStateWithLifecycle() val validationDialog = viewModel.validationDialog.collectAsStateWithLifecycle() GMActionContent( @@ -48,6 +52,11 @@ fun GMActionPage( viewModel.onServerSync() } }, + onEditSession = { + scope.launch { + viewModel.onEditSession() + } + }, onPartyHeal = { scope.launch { viewModel.onPartyHeal() @@ -72,15 +81,20 @@ fun GMActionPage( ConfirmationDialog( dialog = validationDialog, ) + + GMEditDialog( + dialog = editDialog + ) } @Composable fun GMActionContent( modifier: Modifier = Modifier, scroll: ScrollState, - spacing: Dp = 8.dp, + spacing: Dp = MaterialTheme.lwa.dimen.paddingValue, actions: State, onServerSync: () -> Unit, + onEditSession: () -> Unit, onPartyHeal: () -> Unit, onPartyVisibility: () -> Unit, onNpcVisibility: () -> Unit, @@ -97,6 +111,12 @@ fun GMActionContent( label = "Synchronization du serveur", onAction = onServerSync, ) + GMAction( + modifier = Modifier.fillMaxWidth(), + icon = Res.drawable.ic_edit_note_24dp, + label = "Edition du titre de session", + onAction = onEditSession, + ) GMAction( modifier = Modifier.fillMaxWidth(), icon = Res.drawable.ic_camping_24dp, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionUseCase.kt index 895f9a0..e630144 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionUseCase.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionUseCase.kt @@ -19,6 +19,16 @@ class GMActionUseCase( ) } + fun currentSceneTitle(): String = campaignRepository.campaignFlow().value.scene.name + + suspend fun updateSceneTitle( + title: String, + ) { + campaignRepository.updateSceneTitle( + title = title, + ) + } + suspend fun healPlayerParty() { campaignRepository.campaignFlow().value.characters.forEach { characterSheetId -> val sheet = characterRepository.characterDetail( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt index c80f8be..e740823 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.ui.composable.confirmation.ConfirmationDialogUio import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -15,6 +17,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__actions__campaign_scene__title 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 @@ -27,7 +30,6 @@ import lwacharactersheet.composeapp.generated.resources.game_master__actions__sh 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( @@ -55,11 +57,14 @@ class GMActionViewModel( private val _validationDialog = MutableStateFlow(null) val validationDialog: StateFlow = _validationDialog + private val _editDialog = MutableStateFlow(null) + val editDialog: StateFlow = _editDialog + suspend fun onServerSync() { showConfirmationDialog( - title = Res.string.game_master__actions__on_server_sync__title, - description = Res.string.game_master__actions__on_server_sync__description, - onConfirmationRequest = { + title = getString(Res.string.game_master__actions__on_server_sync__title), + description = getString(Res.string.game_master__actions__on_server_sync__description), + onConfirmRequest = { try { actionUseCase.invalidateServerCache() } catch (exception: Exception) { @@ -73,11 +78,29 @@ class GMActionViewModel( ) } + suspend fun onEditSession() { + showEditDialog( + title = getString(Res.string.game_master__actions__campaign_scene__title), + value = actionUseCase.currentSceneTitle(), + onConfirmRequest = { + try { + actionUseCase.updateSceneTitle(title = it) + } catch (exception: Exception) { + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + } + }, + onDismissRequest = { + _editDialog.value = null + }, + ) + } + suspend fun onPartyHeal() { showConfirmationDialog( - title = Res.string.game_master__actions__party_heal__title, - description = Res.string.game_master__actions__party_heal__description, - onConfirmationRequest = { + title = getString(Res.string.game_master__actions__party_heal__title), + description = getString(Res.string.game_master__actions__party_heal__description), + onConfirmRequest = { try { actionUseCase.healPlayerParty() } catch (exception: Exception) { @@ -91,14 +114,14 @@ class GMActionViewModel( suspend fun onPlayerVisibility() { showConfirmationDialog( title = when (actions.value?.party) { - true -> Res.string.game_master__actions__hide_player__title - else -> Res.string.game_master__actions__show_player__title + true -> getString(Res.string.game_master__actions__hide_player__title) + else -> getString(Res.string.game_master__actions__show_player__title) }, description = when (actions.value?.party) { - true -> Res.string.game_master__actions__hide_player__description - else -> Res.string.game_master__actions__show_player__description + true -> getString(Res.string.game_master__actions__hide_player__description) + else -> getString(Res.string.game_master__actions__show_player__description) }, - onConfirmationRequest = { + onConfirmRequest = { try { actionUseCase.togglePlayerVisibility() } catch (exception: Exception) { @@ -112,14 +135,14 @@ class GMActionViewModel( 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 + true -> getString(Res.string.game_master__actions__hide_npc__title) + else -> getString(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 + true -> getString(Res.string.game_master__actions__hide_npc__description) + else -> getString(Res.string.game_master__actions__show_npc__description) }, - onConfirmationRequest = { + onConfirmRequest = { try { actionUseCase.toggleNpcVisibility() } catch (exception: Exception) { @@ -130,18 +153,42 @@ class GMActionViewModel( ) } - private suspend inline fun showConfirmationDialog( - title: StringResource, - description: StringResource, - crossinline onConfirmationRequest: suspend () -> Unit, + private inline fun showConfirmationDialog( + title: String, + description: String, + crossinline onConfirmRequest: suspend () -> Unit, crossinline onDismissRequest: () -> Unit = { _validationDialog.value = null }, ) { _validationDialog.value = ConfirmationDialogUio( - title = getString(title), - description = getString(description), + title = title, + description = description, onConfirmRequest = { viewModelScope.launch { - onConfirmationRequest() + onConfirmRequest() + onDismissRequest() + } + }, + onDismissRequest = { + onDismissRequest() + }, + ) + } + + private inline fun showEditDialog( + title: String, + value: String, + crossinline onConfirmRequest: suspend (String) -> Unit, + crossinline onDismissRequest: () -> Unit = { _editDialog.value = null }, + ) { + val edit = createLwaTextFieldFlow( + label = value, + ) + _editDialog.value = GMEditDialogUio( + title = title, + edit = edit.createLwaTextField(), + onConfirmRequest = { + viewModelScope.launch { + onConfirmRequest(edit.valueFlow.value) onDismissRequest() } }, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMEditDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMEditDialog.kt new file mode 100644 index 0000000..8b519c2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMEditDialog.kt @@ -0,0 +1,97 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.action + +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.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 com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +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 GMEditDialogUio( + val title: String, + val edit: LwaTextFieldUio, + val onConfirmRequest: () -> Unit, + val onDismissRequest: () -> Unit, +) + +@Stable +object GMEditDialogDefault { + + @Stable + val paddings = PaddingValues(start = 16.dp, top = 16.dp, end = 16.dp) + + @Stable + val spacings: Dp = 8.dp +} + +@Composable +fun GMEditDialog( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = GMEditDialogDefault.paddings, + spacing: Dp = GMEditDialogDefault.spacings, + dialog: State, +) { + 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, + ) + LwaTextField( + field = it.edit, + ) + 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) + ) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/LayoutDirection+Invert.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/LayoutDirection+Invert.kt index 35ca892..7600a6b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/LayoutDirection+Invert.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/LayoutDirection+Invert.kt @@ -1,8 +1,10 @@ package com.pixelized.desktop.lwa.utils.extention +import androidx.compose.runtime.Stable import androidx.compose.ui.unit.LayoutDirection val LayoutDirection.invert: LayoutDirection + @Stable get() = when (this) { LayoutDirection.Ltr -> LayoutDirection.Rtl LayoutDirection.Rtl -> LayoutDirection.Ltr diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt index ddda468..1a4314d 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt @@ -29,6 +29,14 @@ class CampaignJsonFactory( // Json conversion. + fun createScene( + title: String, + ): CampaignJsonV2.SceneJsonV2 { + return CampaignJsonV2.SceneJsonV2( + name = title, + ) + } + fun convertToJson( campaign: Campaign, ): CampaignJson {