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__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__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__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>

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.purse.PurseDialogFactory
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.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.header.CharacterDetailHeaderFactory
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.PlayerRibbonViewModel
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.toolbar.CampaignToolbarViewModel
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.action.GMActionUseCase
import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel
@ -112,6 +113,7 @@ val toolsDependencies
single {
PathProvider(appName = "LwaClient")
}
singleOf(::ImagerModelConverter)
}
val storeDependencies

View file

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

View file

@ -58,6 +58,16 @@ class LwaClientImpl(
.get("$root/campaign")
.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
override suspend fun putCampaignCharacter(
characterSheetId: String,

View file

@ -14,6 +14,12 @@ class CampaignRepository(
store.updateCampaignFlow()
}
suspend fun updateSceneTitle(
title: String,
) {
store.changeSceneTitle(title = title)
}
@Throws
suspend fun addCharacter(
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
suspend fun addCharacter(
characterSheetId: String,

View file

@ -6,12 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
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 com.pixelized.desktop.lwa.utils.rememberBackgroundGradient
@ -32,8 +30,10 @@ fun DesaturatedAsyncImage(
filterQuality: FilterQuality = FilterQuality.Low,
clipToBounds: Boolean = true,
) {
Box(modifier = modifier) {
AsyncImage(
Box(
modifier = modifier,
) {
LwaAsyncImage(
model = model,
contentDescription = contentDescription,
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.unit.dp
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.lwa
import kotlinx.coroutines.launch
@ -97,7 +98,7 @@ private fun PortraitContent(
else -> Box(
modifier = Modifier.fillMaxSize(),
) {
AsyncImage(
LwaAsyncImage(
modifier = Modifier.matchParentSize(),
model = it,
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
@ -360,14 +361,24 @@ private fun CampaignLayout(
) {
leftPanel()
}
Column(
modifier = Modifier.weight(weight = 1f, fill = true),
) {
Box(
modifier = Modifier
.align(alignment = Alignment.Bottom)
.weight(weight = 1f, fill = true)
.fillMaxWidth()
.weight(weight = 1f, fill = true),
) {
overlay()
}
Box(
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { chatOverlayState.value = it.toDp(density) },
) {
chat()
}
}
Box(
modifier = Modifier
.width(width = MaterialTheme.lwa.dimen.layout.panelWidth)
@ -377,13 +388,6 @@ private fun CampaignLayout(
rightPanel()
}
}
Box(
modifier = Modifier
.align(alignment = Alignment.Center)
.fillMaxSize(),
) {
overlay()
}
Box(
modifier = Modifier
.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.dp
import coil3.PlatformContext
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
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.LwaAsyncImage
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.calculatePaddings
@ -194,7 +194,8 @@ fun InventoryItem(
shape = CircleShape,
)
)
AsyncImage(
LwaAsyncImage(
modifier = Modifier
.size(size = icon)
.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.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
import coil3.compose.AsyncImage
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.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
},
)
},
@ -79,7 +74,7 @@ fun CharacterRibbonAlteration(
targetState = it.icon,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { icon ->
AsyncImage(
LwaAsyncImage(
modifier = Modifier.size(24.dp),
model = ImageRequest.Builder(context = PlatformContext.INSTANCE)
.data(data = icon)

View file

@ -6,7 +6,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.onClick
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
@ -30,12 +28,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
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.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
@ -80,7 +77,7 @@ fun CharacterRibbonPortrait(
targetState = character.portrait,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { portrait ->
AsyncImage(
LwaAsyncImage(
modifier = Modifier.fillMaxSize(),
model = portrait,
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.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<ActionPageUio?>,
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,

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() {
campaignRepository.campaignFlow().value.characters.forEach { characterSheetId ->
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.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<ConfirmationDialogUio?>(null)
val validationDialog: StateFlow<ConfirmationDialogUio?> = _validationDialog
private val _editDialog = MutableStateFlow<GMEditDialogUio?>(null)
val editDialog: StateFlow<GMEditDialogUio?> = _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()
}
},

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.unit.dp
import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import com.pixelized.desktop.lwa.LocalRollHostState
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.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.LevelUpDestination
@ -204,7 +204,7 @@ private fun LevelUpContent(
)
},
background = {
AsyncImage(
LwaAsyncImage(
modifier = Modifier.matchParentSize(),
model = header.value?.portrait,
contentScale = ContentScale.FillHeight,

View file

@ -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

View file

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