Compare commits

...

3 commits

22 changed files with 358 additions and 69 deletions

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#5f6368"
android:pathData="M160,560L160,480L440,480L440,560L160,560ZM160,400L160,320L600,320L600,400L160,400ZM160,240L160,160L600,160L600,240L160,240ZM520,800L520,677L741,457Q750,448 761,444Q772,440 783,440Q795,440 806,444.5Q817,449 826,458L863,495Q871,504 875.5,515Q880,526 880,537Q880,548 876,559.5Q872,571 863,580L643,800L520,800ZM820,537L820,537L783,500L783,500L820,537ZM580,740L618,740L739,618L721,599L702,581L580,702L580,740ZM721,599L702,581L702,581L739,618L739,618L721,599Z" />
</vector>

View file

@ -325,6 +325,7 @@
<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__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__on_server_sync__description">Demander au serveur d'invalider son cache</string>
<string name="game_master__actions__campaign_scene__title">Titre de la scène</string>
<string name="game_master__actions__party_heal__title">Soigner les personnages joueurs</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__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__title">Cacher le groupe de personnages joueur</string>

View file

@ -31,21 +31,22 @@ import com.pixelized.desktop.lwa.ui.composable.character.item.ItemDetailDialogFa
import com.pixelized.desktop.lwa.ui.composable.character.item.ItemDetailDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.item.ItemDetailDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogFactory 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.composable.character.purse.PurseDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.image.ImagerModelConverter
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.toolbar.network.NetworkFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory 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.detail.inventory.CharacterDetailInventoryFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel
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.text.TextMessageFactory 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.campaign.toolbar.network.NetworkFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkViewModel
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.GMActionUseCase
import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel
@ -112,6 +113,7 @@ val toolsDependencies
single { single {
PathProvider(appName = "LwaClient") PathProvider(appName = "LwaClient")
} }
singleOf(::ImagerModelConverter)
} }
val storeDependencies val storeDependencies

View file

@ -33,6 +33,10 @@ interface LwaClient {
suspend fun getCampaign(): APIResponse<CampaignJson> suspend fun getCampaign(): APIResponse<CampaignJson>
suspend fun putCampaignScene(
scene: CampaignJson.SceneJson,
): APIResponse<Unit>
suspend fun putCampaignCharacter( suspend fun putCampaignCharacter(
characterSheetId: String, characterSheetId: String,
): APIResponse<Unit> ): APIResponse<Unit>

View file

@ -58,6 +58,16 @@ class LwaClientImpl(
.get("$root/campaign") .get("$root/campaign")
.body() .body()
@Throws
override suspend fun putCampaignScene(
scene: CampaignJson.SceneJson,
): APIResponse<Unit> = client
.put("$root/campaign/scene") {
contentType(ContentType.Application.Json)
setBody(scene)
}
.body<APIResponse<Unit>>()
@Throws @Throws
override suspend fun putCampaignCharacter( override suspend fun putCampaignCharacter(
characterSheetId: String, characterSheetId: String,

View file

@ -14,6 +14,12 @@ class CampaignRepository(
store.updateCampaignFlow() store.updateCampaignFlow()
} }
suspend fun updateSceneTitle(
title: String,
) {
store.changeSceneTitle(title = title)
}
@Throws @Throws
suspend fun addCharacter( suspend fun addCharacter(
characterSheetId: String, characterSheetId: String,

View file

@ -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 @Throws
suspend fun addCharacter( suspend fun addCharacter(
characterSheetId: String, characterSheetId: String,

View file

@ -6,12 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter.Companion.DefaultTransform import coil3.compose.AsyncImagePainter.Companion.DefaultTransform
import coil3.compose.AsyncImagePainter.State import coil3.compose.AsyncImagePainter.State
import com.pixelized.desktop.lwa.utils.rememberBackgroundGradient import com.pixelized.desktop.lwa.utils.rememberBackgroundGradient
@ -32,8 +30,10 @@ fun DesaturatedAsyncImage(
filterQuality: FilterQuality = FilterQuality.Low, filterQuality: FilterQuality = FilterQuality.Low,
clipToBounds: Boolean = true, clipToBounds: Boolean = true,
) { ) {
Box(modifier = modifier) { Box(
AsyncImage( modifier = modifier,
) {
LwaAsyncImage(
model = model, model = model,
contentDescription = contentDescription, contentDescription = contentDescription,
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),

View file

@ -0,0 +1,19 @@
package com.pixelized.desktop.lwa.ui.composable.image
class ImagerModelConverter {
val googleDriveUrlRegex = Regex("""drive\.google\.com/file/d/([^/]*)""")
val workingGoogleDriveUri = "https://drive.google.com/uc?export=view&id="
fun convert(
model: Any?,
): Any? {
return when (model) {
is String -> googleDriveUrlRegex.find(model)?.let {
val id = it.groupValues.getOrNull(1)
"$workingGoogleDriveUri$id"
} ?: model
else -> model
}
}
}

View file

@ -0,0 +1,44 @@
package com.pixelized.desktop.lwa.ui.composable.image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter.Companion.DefaultTransform
import coil3.compose.AsyncImagePainter.State
import org.koin.compose.koinInject
@Composable
fun LwaAsyncImage(
model: Any?,
modelConverter: ImagerModelConverter? = koinInject<ImagerModelConverter?>(),
contentDescription: String?,
modifier: Modifier = Modifier,
transform: (State) -> State = DefaultTransform,
onState: ((State) -> Unit)? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = FilterQuality.Low,
clipToBounds: Boolean = true,
) {
AsyncImage(
modifier = modifier,
model = remember(modelConverter, model) { modelConverter?.convert(model) ?: model },
contentDescription = contentDescription,
transform = transform,
onState = onState,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality,
clipToBounds = clipToBounds,
)
}

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.FilterQuality
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 coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.pixelized.desktop.lwa.ui.composable.image.LwaAsyncImage
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -97,7 +98,7 @@ private fun PortraitContent(
else -> Box( else -> Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
AsyncImage( LwaAsyncImage(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
model = it, model = it,
filterQuality = FilterQuality.High, filterQuality = FilterQuality.High,

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -360,14 +361,24 @@ private fun CampaignLayout(
) { ) {
leftPanel() leftPanel()
} }
Column(
modifier = Modifier.weight(weight = 1f, fill = true),
) {
Box( Box(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.Bottom) .fillMaxWidth()
.weight(weight = 1f, fill = true) .weight(weight = 1f, fill = true),
) {
overlay()
}
Box(
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { chatOverlayState.value = it.toDp(density) }, .onSizeChanged { chatOverlayState.value = it.toDp(density) },
) { ) {
chat() chat()
} }
}
Box( Box(
modifier = Modifier modifier = Modifier
.width(width = MaterialTheme.lwa.dimen.layout.panelWidth) .width(width = MaterialTheme.lwa.dimen.layout.panelWidth)
@ -377,13 +388,6 @@ private fun CampaignLayout(
rightPanel() rightPanel()
} }
} }
Box(
modifier = Modifier
.align(alignment = Alignment.Center)
.fillMaxSize(),
) {
overlay()
}
Box( Box(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.CenterStart) .align(alignment = Alignment.CenterStart)

View file

@ -44,10 +44,10 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.PlatformContext import coil3.PlatformContext
import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.image.DesaturatedAsyncImage import com.pixelized.desktop.lwa.ui.composable.image.DesaturatedAsyncImage
import com.pixelized.desktop.lwa.ui.composable.image.LwaAsyncImage
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.calculatePaddings import com.pixelized.desktop.lwa.utils.extention.calculatePaddings
@ -194,7 +194,8 @@ fun InventoryItem(
shape = CircleShape, shape = CircleShape,
) )
) )
AsyncImage(
LwaAsyncImage(
modifier = Modifier modifier = Modifier
.size(size = icon) .size(size = icon)
.aspectRatio(ratio = 1f, matchHeightConstraintsFirst = true), .aspectRatio(ratio = 1f, matchHeightConstraintsFirst = true),

View file

@ -21,19 +21,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.PlatformContext import coil3.PlatformContext
import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.pixelized.desktop.lwa.ui.composable.image.LwaAsyncImage
import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipLayout import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipUio import com.pixelized.desktop.lwa.ui.composable.tooltip.BasicTooltipUio
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.invert
@Stable @Stable
data class CharacterRibbonAlterationUio( data class CharacterRibbonAlterationUio(
val icon: String, val icon: String,
val tooltips: BasicTooltipUio?, val tooltips: BasicTooltipUio?,
) )
@ -63,14 +62,10 @@ fun CharacterRibbonAlteration(
delayMillis = 0, delayMillis = 0,
tooltip = it.tooltips, tooltip = it.tooltips,
tooltipPlacement = remember(direction) { tooltipPlacement = remember(direction) {
TooltipPlacement.ComponentRect( TooltipPlacement.CursorPoint(
anchor = when (direction) { alignment = when (direction.invert) {
LayoutDirection.Ltr -> Alignment.CenterEnd LayoutDirection.Ltr -> Alignment.CenterStart
LayoutDirection.Rtl -> Alignment.CenterStart LayoutDirection.Rtl -> Alignment.CenterEnd
},
alignment = when (direction) {
LayoutDirection.Ltr -> Alignment.CenterEnd
LayoutDirection.Rtl -> Alignment.CenterStart
}, },
) )
}, },
@ -79,7 +74,7 @@ fun CharacterRibbonAlteration(
targetState = it.icon, targetState = it.icon,
transitionSpec = { fadeIn() togetherWith fadeOut() }, transitionSpec = { fadeIn() togetherWith fadeOut() },
) { icon -> ) { icon ->
AsyncImage( LwaAsyncImage(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
model = ImageRequest.Builder(context = PlatformContext.INSTANCE) model = ImageRequest.Builder(context = PlatformContext.INSTANCE)
.data(data = icon) .data(data = icon)

View file

@ -6,7 +6,6 @@ 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.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -18,7 +17,6 @@ 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
@ -30,12 +28,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import com.pixelized.desktop.lwa.ui.composable.image.LwaAsyncImage
import com.pixelized.desktop.lwa.ui.composable.shapes.ArrowShape import com.pixelized.desktop.lwa.ui.composable.shapes.ArrowShape
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
@ -80,7 +77,7 @@ fun CharacterRibbonPortrait(
targetState = character.portrait, targetState = character.portrait,
transitionSpec = { fadeIn() togetherWith fadeOut() }, transitionSpec = { fadeIn() togetherWith fadeOut() },
) { portrait -> ) { portrait ->
AsyncImage( LwaAsyncImage(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
model = portrait, model = portrait,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,

View file

@ -7,6 +7,7 @@ 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.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
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
@ -15,11 +16,13 @@ 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 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.confirmation.ConfirmationDialog
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import com.pixelized.desktop.lwa.ui.theme.lwa
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
import lwacharactersheet.composeapp.generated.resources.ic_edit_note_24dp
import lwacharactersheet.composeapp.generated.resources.ic_sync_24dp import lwacharactersheet.composeapp.generated.resources.ic_sync_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp
@ -38,6 +41,7 @@ fun GMActionPage(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scroll = rememberScrollState() val scroll = rememberScrollState()
val actions = viewModel.actions.collectAsStateWithLifecycle() val actions = viewModel.actions.collectAsStateWithLifecycle()
val editDialog = viewModel.editDialog.collectAsStateWithLifecycle()
val validationDialog = viewModel.validationDialog.collectAsStateWithLifecycle() val validationDialog = viewModel.validationDialog.collectAsStateWithLifecycle()
GMActionContent( GMActionContent(
@ -48,6 +52,11 @@ fun GMActionPage(
viewModel.onServerSync() viewModel.onServerSync()
} }
}, },
onEditSession = {
scope.launch {
viewModel.onEditSession()
}
},
onPartyHeal = { onPartyHeal = {
scope.launch { scope.launch {
viewModel.onPartyHeal() viewModel.onPartyHeal()
@ -72,15 +81,20 @@ fun GMActionPage(
ConfirmationDialog( ConfirmationDialog(
dialog = validationDialog, dialog = validationDialog,
) )
GMEditDialog(
dialog = editDialog
)
} }
@Composable @Composable
fun GMActionContent( fun GMActionContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
scroll: ScrollState, scroll: ScrollState,
spacing: Dp = 8.dp, spacing: Dp = MaterialTheme.lwa.dimen.paddingValue,
actions: State<ActionPageUio?>, actions: State<ActionPageUio?>,
onServerSync: () -> Unit, onServerSync: () -> Unit,
onEditSession: () -> Unit,
onPartyHeal: () -> Unit, onPartyHeal: () -> Unit,
onPartyVisibility: () -> Unit, onPartyVisibility: () -> Unit,
onNpcVisibility: () -> Unit, onNpcVisibility: () -> Unit,
@ -97,6 +111,12 @@ fun GMActionContent(
label = "Synchronization du serveur", label = "Synchronization du serveur",
onAction = onServerSync, onAction = onServerSync,
) )
GMAction(
modifier = Modifier.fillMaxWidth(),
icon = Res.drawable.ic_edit_note_24dp,
label = "Edition du titre de session",
onAction = onEditSession,
)
GMAction( GMAction(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
icon = Res.drawable.ic_camping_24dp, icon = Res.drawable.ic_camping_24dp,

View file

@ -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() { suspend fun healPlayerParty() {
campaignRepository.campaignFlow().value.characters.forEach { characterSheetId -> campaignRepository.campaignFlow().value.characters.forEach { characterSheetId ->
val sheet = characterRepository.characterDetail( val sheet = characterRepository.characterDetail(

View file

@ -5,6 +5,8 @@ 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.ui.composable.confirmation.ConfirmationDialogUio 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.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.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@ -15,6 +17,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
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.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__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__hide_npc__title 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__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_npc__title
import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_player__description import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_player__description
import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_player__title import lwacharactersheet.composeapp.generated.resources.game_master__actions__show_player__title
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
class GMActionViewModel( class GMActionViewModel(
@ -55,11 +57,14 @@ class GMActionViewModel(
private val _validationDialog = MutableStateFlow<ConfirmationDialogUio?>(null) private val _validationDialog = MutableStateFlow<ConfirmationDialogUio?>(null)
val validationDialog: StateFlow<ConfirmationDialogUio?> = _validationDialog val validationDialog: StateFlow<ConfirmationDialogUio?> = _validationDialog
private val _editDialog = MutableStateFlow<GMEditDialogUio?>(null)
val editDialog: StateFlow<GMEditDialogUio?> = _editDialog
suspend fun onServerSync() { suspend fun onServerSync() {
showConfirmationDialog( showConfirmationDialog(
title = Res.string.game_master__actions__on_server_sync__title, title = getString(Res.string.game_master__actions__on_server_sync__title),
description = Res.string.game_master__actions__on_server_sync__description, description = getString(Res.string.game_master__actions__on_server_sync__description),
onConfirmationRequest = { onConfirmRequest = {
try { try {
actionUseCase.invalidateServerCache() actionUseCase.invalidateServerCache()
} catch (exception: Exception) { } 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() { suspend fun onPartyHeal() {
showConfirmationDialog( showConfirmationDialog(
title = Res.string.game_master__actions__party_heal__title, title = getString(Res.string.game_master__actions__party_heal__title),
description = Res.string.game_master__actions__party_heal__description, description = getString(Res.string.game_master__actions__party_heal__description),
onConfirmationRequest = { onConfirmRequest = {
try { try {
actionUseCase.healPlayerParty() actionUseCase.healPlayerParty()
} catch (exception: Exception) { } catch (exception: Exception) {
@ -91,14 +114,14 @@ class GMActionViewModel(
suspend fun onPlayerVisibility() { suspend fun onPlayerVisibility() {
showConfirmationDialog( showConfirmationDialog(
title = when (actions.value?.party) { title = when (actions.value?.party) {
true -> Res.string.game_master__actions__hide_player__title true -> getString(Res.string.game_master__actions__hide_player__title)
else -> Res.string.game_master__actions__show_player__title else -> getString(Res.string.game_master__actions__show_player__title)
}, },
description = when (actions.value?.party) { description = when (actions.value?.party) {
true -> Res.string.game_master__actions__hide_player__description true -> getString(Res.string.game_master__actions__hide_player__description)
else -> Res.string.game_master__actions__show_player__description else -> getString(Res.string.game_master__actions__show_player__description)
}, },
onConfirmationRequest = { onConfirmRequest = {
try { try {
actionUseCase.togglePlayerVisibility() actionUseCase.togglePlayerVisibility()
} catch (exception: Exception) { } catch (exception: Exception) {
@ -112,14 +135,14 @@ class GMActionViewModel(
suspend fun onNpcVisibility() { suspend fun onNpcVisibility() {
showConfirmationDialog( showConfirmationDialog(
title = when (actions.value?.npc) { title = when (actions.value?.npc) {
true -> Res.string.game_master__actions__hide_npc__title true -> getString(Res.string.game_master__actions__hide_npc__title)
else -> Res.string.game_master__actions__show_npc__title else -> getString(Res.string.game_master__actions__show_npc__title)
}, },
description = when (actions.value?.npc) { description = when (actions.value?.npc) {
true -> Res.string.game_master__actions__hide_npc__description true -> getString(Res.string.game_master__actions__hide_npc__description)
else -> Res.string.game_master__actions__show_npc__description else -> getString(Res.string.game_master__actions__show_npc__description)
}, },
onConfirmationRequest = { onConfirmRequest = {
try { try {
actionUseCase.toggleNpcVisibility() actionUseCase.toggleNpcVisibility()
} catch (exception: Exception) { } catch (exception: Exception) {
@ -130,18 +153,42 @@ class GMActionViewModel(
) )
} }
private suspend inline fun showConfirmationDialog( private inline fun showConfirmationDialog(
title: StringResource, title: String,
description: StringResource, description: String,
crossinline onConfirmationRequest: suspend () -> Unit, crossinline onConfirmRequest: suspend () -> Unit,
crossinline onDismissRequest: () -> Unit = { _validationDialog.value = null }, crossinline onDismissRequest: () -> Unit = { _validationDialog.value = null },
) { ) {
_validationDialog.value = ConfirmationDialogUio( _validationDialog.value = ConfirmationDialogUio(
title = getString(title), title = title,
description = getString(description), description = description,
onConfirmRequest = { onConfirmRequest = {
viewModelScope.launch { 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() onDismissRequest()
} }
}, },

View file

@ -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<GMEditDialogUio?>,
) {
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)
)
}
}
}
}
}

View file

@ -60,9 +60,9 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import com.pixelized.desktop.lwa.LocalRollHostState import com.pixelized.desktop.lwa.LocalRollHostState
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.image.LwaAsyncImage
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.LevelUpDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.LevelUpDestination
@ -204,7 +204,7 @@ private fun LevelUpContent(
) )
}, },
background = { background = {
AsyncImage( LwaAsyncImage(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
model = header.value?.portrait, model = header.value?.portrait,
contentScale = ContentScale.FillHeight, contentScale = ContentScale.FillHeight,

View file

@ -1,8 +1,10 @@
package com.pixelized.desktop.lwa.utils.extention package com.pixelized.desktop.lwa.utils.extention
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
val LayoutDirection.invert: LayoutDirection val LayoutDirection.invert: LayoutDirection
@Stable
get() = when (this) { get() = when (this) {
LayoutDirection.Ltr -> LayoutDirection.Rtl LayoutDirection.Ltr -> LayoutDirection.Rtl
LayoutDirection.Rtl -> LayoutDirection.Ltr LayoutDirection.Rtl -> LayoutDirection.Ltr

View file

@ -29,6 +29,14 @@ class CampaignJsonFactory(
// Json conversion. // Json conversion.
fun createScene(
title: String,
): CampaignJsonV2.SceneJsonV2 {
return CampaignJsonV2.SceneJsonV2(
name = title,
)
}
fun convertToJson( fun convertToJson(
campaign: Campaign, campaign: Campaign,
): CampaignJson { ): CampaignJson {