Compare commits

...

3 commits

39 changed files with 1102 additions and 474 deletions

View file

@ -34,6 +34,7 @@ kotlin {
// injection
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.engawapg.zoomable)
// composable component.
implementation(libs.coil.compose)
implementation(libs.coil.network.ktor)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M480,640Q414,640 367,593Q320,546 320,480Q320,414 367,367Q414,320 480,320Q546,320 593,367Q640,414 640,480Q640,546 593,593Q546,640 480,640ZM480,560Q513,560 536.5,536.5Q560,513 560,480Q560,447 536.5,423.5Q513,400 480,400Q447,400 423.5,423.5Q400,447 400,480Q400,513 423.5,536.5Q447,560 480,560ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,600L200,600L200,760Q200,760 200,760Q200,760 200,760L360,760L360,840L200,840ZM600,840L600,760L760,760Q760,760 760,760Q760,760 760,760L760,600L840,600L840,760Q840,793 816.5,816.5Q793,840 760,840L600,840ZM120,360L120,200Q120,167 143.5,143.5Q167,120 200,120L360,120L360,200L200,200Q200,200 200,200Q200,200 200,200L200,360L120,360ZM760,360L760,200Q760,200 760,200Q760,200 760,200L600,200L600,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,360L760,360Z" />
</vector>

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#5f6368">
<path d="M200-200v-440h80v360h360v80H200Zm200-200v-440h80v360h360v80H400Z" />
</svg>

After

Width:  |  Height:  |  Size: 212 B

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M156,860L100,804L224,680L120,680L120,600L360,600L360,840L280,840L280,736L156,860ZM804,860L680,736L680,840L600,840L600,600L840,600L840,680L736,680L860,804L804,860ZM120,360L120,280L224,280L100,156L156,100L280,224L280,120L360,120L360,360L120,360ZM600,360L600,120L680,120L680,224L804,100L860,156L736,280L840,280L840,360L600,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M120,840L120,600L200,600L200,704L324,580L380,636L256,760L360,760L360,840L120,840ZM600,840L600,760L704,760L580,636L636,580L760,704L760,600L840,600L840,840L600,840ZM324,380L200,256L200,360L120,360L120,120L360,120L360,200L256,200L380,324L324,380ZM636,380L580,324L704,200L600,200L600,120L840,120L840,360L760,360L760,256L636,380Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -269,12 +269,6 @@
<string name="settings__player_portrait__dyn_dice_delay_tile">Délai pour les Dés dynamiques</string>
<string name="settings__player_portrait__dyn_dice_delay_description">Délai après lequel les dés dynamiques disparaissent.</string>
<string name="settings__chat_log__section">Chatlog options</string>
<string name="settings__chat_log__auto_show_title">Afficher automatiquement le chat</string>
<string name="settings__chat_log__auto_show_description">Affiche automatiquement le chat lors de la réception d'un message</string>
<string name="settings__chat_log__auto_hide_title">Cacher automatiquement le chat</string>
<string name="settings__chat_log__auto_hide_description">Cache automatiquement le chat au bout d'un certain temps</string>
<string name="settings__chat_log__auto_hide_delay_title">Délai pour cacher le chat</string>
<string name="settings__chat_log__auto_hide_delay_description">Délai après lequel le chat disparaît</string>
<string name="settings__chat_log__auto_scroll_title">Défilement automatique</string>
<string name="settings__chat_log__auto_scroll_description">Défilement automatique du chat vers le dernier message reçu lors de la réception de ce dernier.</string>
<string name="settings__chat_log__line_count_title">Nombre de lignes de textes visibles</string>
@ -329,5 +323,17 @@
<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__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>

View file

@ -40,13 +40,14 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.Characte
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
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.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.alteration.edit.GMAlterationEditFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditViewModel
@ -198,4 +199,5 @@ val viewModelDependencies
val useCaseDependencies
get() = module {
factoryOf(::SettingsUseCase)
factoryOf(::GMActionUseCase)
}

View file

@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.repository.settings
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJsonV1
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJsonV2
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
@ -12,15 +13,13 @@ class SettingsFactory(
fun convertToJson(
settings: Settings,
): SettingsJson {
return SettingsJsonV1(
return SettingsJsonV2(
host = settings.network.host,
port = settings.network.port,
playerName = settings.playerName,
dynamicDice = settings.portrait.dynamicDice,
dynamicDiceDelay = settings.portrait.dynamicDiceDelay,
autoHideChat = settings.chat.autoHideChat,
autoHideDelay = settings.chat.autoHideDelay,
autoShowChat = settings.chat.autoShowChat,
showChat = settings.chat.showChat,
autoScrollChat = settings.chat.autoScrollChat,
maxLineCount = settings.chat.maxLineCount,
isAdmin = settings.isAdmin,
@ -33,6 +32,7 @@ class SettingsFactory(
): Settings {
return when (json) {
is SettingsJsonV1 -> convertFromJsonV1(json)
is SettingsJsonV2 -> convertFromJsonV2(json)
}
}
@ -51,9 +51,31 @@ class SettingsFactory(
dynamicDiceDelay = json.dynamicDiceDelay ?: default.portrait.dynamicDiceDelay,
),
chat = Settings.Chat(
autoHideChat = json.autoHideChat ?: default.chat.autoHideChat,
autoHideDelay = json.autoHideDelay ?: default.chat.autoHideDelay,
autoShowChat = json.autoShowChat ?: default.chat.autoShowChat,
showChat = default.chat.showChat,
autoScrollChat = json.autoScrollChat ?: default.chat.autoScrollChat,
maxLineCount = json.maxLineCount ?: default.chat.maxLineCount,
),
isAdmin = json.isAdmin ?: default.isAdmin,
isGameMaster = json.isGameMaster ?: default.isGameMaster,
)
}
private fun convertFromJsonV2(
json: SettingsJsonV2,
): Settings {
val default = useCase.defaultSettings()
return Settings(
playerName = json.playerName ?: default.playerName,
network = Settings.Network(
host = json.host ?: default.network.host,
port = json.port ?: default.network.port,
),
portrait = Settings.Portrait(
dynamicDice = json.dynamicDice ?: default.portrait.dynamicDice,
dynamicDiceDelay = json.dynamicDiceDelay ?: default.portrait.dynamicDiceDelay,
),
chat = Settings.Chat(
showChat = json.showChat ?: default.chat.showChat,
autoScrollChat = json.autoScrollChat ?: default.chat.autoScrollChat,
maxLineCount = json.maxLineCount ?: default.chat.maxLineCount,
),

View file

@ -14,9 +14,7 @@ data class Settings(
)
data class Chat(
val autoHideChat: Boolean,
val autoHideDelay: Int,
val autoShowChat: Boolean,
val showChat: Boolean,
val autoScrollChat: Boolean,
val maxLineCount: Int,
)

View file

@ -0,0 +1,17 @@
package com.pixelized.desktop.lwa.repository.settings.model
import kotlinx.serialization.Serializable
@Serializable
data class SettingsJsonV2(
val host: String?,
val port: Int?,
val playerName: String?,
val dynamicDice: Boolean?,
val dynamicDiceDelay: Int?,
val showChat: Boolean?,
val autoScrollChat: Boolean?,
val maxLineCount: Int?,
val isGameMaster: Boolean?,
val isAdmin: Boolean?,
) : SettingsJson

View file

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

View file

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

@ -0,0 +1,101 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastRoundToInt
@Stable
class Camera(
private val initialZoom: Float = 2f,
private val initialOffset: IntOffset = IntOffset.Zero,
) {
private var _zoom = Animatable(
initialValue = initialZoom,
typeConverter = Float.VectorConverter,
)
val zoom: Float get() = _zoom.value
private var _offset = Animatable(
initialValue = initialOffset,
typeConverter = IntOffset.VectorConverter,
)
val offset: IntOffset by derivedStateOf {
_offset.value + IntOffset(
x = (_sceneSize.width - cameraSizeZoomed.width) / 2,
y = (_sceneSize.height - cameraSizeZoomed.height) / 2,
)
}
private var _sceneSize: IntSize by mutableStateOf(IntSize.Zero)
private var _cameraSize: IntSize by mutableStateOf(IntSize.Zero)
val cameraSize: IntSize get() = _cameraSize
val cameraSizeZoomed: IntSize by derivedStateOf {
IntSize(
width = (cameraSize.width * zoom).fastRoundToInt(),
height = (cameraSize.height * zoom).fastRoundToInt(),
)
}
fun changeSizes(
sceneSize: IntSize,
cameraSize: IntSize,
) {
_cameraSize = cameraSize
_sceneSize = sceneSize
}
suspend fun handlePanning(
delta: Offset,
snap: Boolean,
) {
val value = _offset.value - IntOffset(
x = (delta.x * zoom).fastRoundToInt(),
y = (delta.y * zoom).fastRoundToInt(),
)
when {
snap -> _offset.snapTo(targetValue = value)
else -> _offset.animateTo(targetValue = value)
}
}
suspend fun handleZoom(
zoomIn: Boolean,
power: Float,
snap: Boolean = false,
) {
val value = _zoom.value * when {
zoomIn -> 1f - power
else -> 1f + power
}
when {
snap -> _zoom.snapTo(targetValue = value)
else -> _zoom.animateTo(targetValue = value)
}
}
suspend fun resetPosition(
snap: Boolean = false,
) {
when (snap) {
true -> _offset.snapTo(targetValue = initialOffset)
else -> _offset.animateTo(targetValue = initialOffset)
}
}
suspend fun resetZoom(
snap: Boolean = false,
) {
when (snap) {
true -> _zoom.snapTo(targetValue = initialZoom)
else -> _zoom.animateTo(targetValue = initialZoom)
}
}
}

View file

@ -0,0 +1,9 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
@Stable
data class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f),
)

View file

@ -0,0 +1,41 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@Stable
class Layout(
val texture: ImageBitmap,
val offset: IntOffset = IntOffset.Zero,
val size: IntSize = IntSize(texture.width, texture.height),
private val initialAlpha: Float = 1f,
) {
private val _alpha = Animatable(
initialValue = initialAlpha,
typeConverter = Float.VectorConverter,
)
val alpha get() = _alpha.value
suspend fun alpha(
alpha: Float,
snap: Boolean = false,
) {
when (snap) {
true -> _alpha.snapTo(targetValue = alpha)
else -> _alpha.animateTo(targetValue = alpha)
}
}
suspend fun resetAlpha(
snap: Boolean = false,
) {
when (snap) {
true -> _alpha.snapTo(targetValue = initialAlpha)
else -> _alpha.animateTo(targetValue = initialAlpha)
}
}
}

View file

@ -0,0 +1,278 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isAltPressed
import androidx.compose.ui.input.pointer.isCtrlPressed
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.input.pointer.isTertiaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.icon_d100
import lwacharactersheet.composeapp.generated.resources.image_dahome_maps
import lwacharactersheet.composeapp.generated.resources.image_dahome_regions
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.imageResource
import org.jetbrains.compose.resources.painterResource
import kotlin.math.sign
@Stable
data class Scene(
val camera: Camera,
val layouts: List<Layout>,
val fogOfWar: FogOfWar,
) {
val size: IntSize = IntSize(
width = layouts.maxOf { it.size.width },
height = layouts.maxOf { it.size.height },
)
}
@Composable
fun Scene(
modifier: Modifier,
) {
val campaign = LocalCampaignLayoutScope.current
val scope = rememberCoroutineScope()
val scene = rememberScene(
camera = Camera(
initialZoom = 1f,
initialOffset = IntOffset(x = -150, y = -120),
),
fogOfWar = FogOfWar(),
rememberLayoutFromResource(
resource = Res.drawable.image_dahome_maps,
),
rememberLayoutFromResource(
resource = Res.drawable.image_dahome_regions,
),
rememberLayoutFromResource(
resource = Res.drawable.icon_d100,
offset = IntOffset(x = 1740, y = 910),
),
)
Box(
modifier = modifier
.graphicsLayer { clip = true }
.onCameraControl(scope = scope, scene = scene)
.drawScene(scene = scene)
.fogOfWar(scene = scene)
) {
Column(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(end = campaign.rightPanel.value.width)
.padding(all = 8.dp)
) {
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = true,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_in_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.handleZoom(
zoomIn = false,
power = 0.3f,
)
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_out_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.camera.resetPosition()
}
scope.launch {
scene.camera.resetZoom()
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_center_focus_weak_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
scope.launch {
scene.layouts.getOrNull(1)?.let {
it.alpha(alpha = if (it.alpha == 0f) 1f else 0f)
}
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_visibility_24dp),
contentDescription = null
)
}
}
}
}
@Composable
@Stable
fun rememberLayoutFromResource(
resource: DrawableResource,
offset: IntOffset = IntOffset.Zero,
): Layout {
val texture = imageResource(
resource = resource,
)
return remember(resource) {
Layout(
texture = texture,
offset = offset,
)
}
}
@Composable
@Stable
fun rememberScene(
camera: Camera,
fogOfWar: FogOfWar,
vararg layouts: Layout,
): Scene {
return remember {
Scene(
camera = camera,
layouts = layouts.toList(),
fogOfWar = fogOfWar,
)
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
fun Modifier.onCameraControl(
scope: CoroutineScope,
scene: Scene,
): Modifier {
val offsetDelta = CursorDelta()
return this
.onSizeChanged {
scene.camera.changeSizes(
sceneSize = scene.size,
cameraSize = it,
)
}
.onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
scope.launch {
offsetDelta.handlePositionChange(
event = event,
) { delta ->
when {
event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> scene.camera.handlePanning(
delta = delta,
snap = true,
)
event.keyboardModifiers.isAltPressed -> scene.camera.handleZoom(
zoomIn = delta.y.sign < 0f,
power = 0.025f,
snap = true,
)
}
}
}
}
.onPointerEvent(PointerEventType.Scroll) { event: PointerEvent ->
scope.launch {
scene.camera.handleZoom(
zoomIn = event.changes.first().scrollDelta.y.sign < 0f,
power = 0.15f,
snap = false,
)
}
}
}
fun Modifier.drawScene(
scene: Scene,
): Modifier = this.drawWithCache {
onDrawBehind {
scene.layouts.forEach { layout ->
drawImage(
image = layout.texture,
srcOffset = scene.camera.offset - layout.offset,
srcSize = scene.camera.cameraSizeZoomed,
dstSize = scene.camera.cameraSize,
alpha = layout.alpha,
)
}
}
}
fun Modifier.fogOfWar(
scene: Scene,
): Modifier = this.drawWithCache {
onDrawBehind {
drawRect(color = scene.fogOfWar.color)
}
}
private data class CursorDelta(
var lastDeltaTimestamp: Long = System.currentTimeMillis(),
var previousPosition: Offset = Offset.Zero,
var currentPosition: Offset = Offset.Zero,
) {
suspend inline fun handlePositionChange(
event: PointerEvent,
delay: Float = 10f,
crossinline block: suspend (delta: Offset) -> Unit,
) {
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastDeltaTimestamp > delay) {
lastDeltaTimestamp = currentTimestamp
previousPosition = currentPosition
currentPosition = event.changes.first().position
block(currentPosition - previousPosition)
}
}
}

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
@ -56,7 +56,7 @@ fun BasicTooltipLayout(
tips = tooltip,
tooltip = {
BasicTooltip(
modifier = Modifier.width(width = 448.dp),
modifier = Modifier.widthIn(max = 448.dp),
elevation = elevation,
tooltip = it,
)
@ -72,7 +72,9 @@ private fun BasicTooltip(
tooltip: BasicTooltipUio,
) {
Surface(
modifier = Modifier.padding(16.dp).then(other = modifier),
modifier = Modifier
.padding(16.dp)
.then(other = modifier),
color = MaterialTheme.colors.surface,
elevation = elevation,
shape = remember { RoundedCornerShape(4.dp) }

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@ -19,6 +20,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isAltPressed
import androidx.compose.ui.input.key.isCtrlPressed
@ -41,6 +43,7 @@ import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterShe
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.composable.scene.Scene
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay
@ -48,13 +51,14 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetai
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbon
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.CampaignChat
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch
import org.koin.compose.viewmodel.koinViewModel
@ -66,6 +70,7 @@ val LocalCampaignLayoutScope = compositionLocalOf<CampaignLayoutScope> {
fun CampaignScreen(
playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(),
playerDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "player"),
npcRibbonViewModel: NpcRibbonViewModel = koinViewModel(),
npcDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "npc"),
characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterSheetDiminishedDialogViewModel = koinViewModel(),
@ -95,7 +100,9 @@ fun CampaignScreen(
},
main = {
Scene(
modifier = Modifier.matchParentSize(),
)
},
chat = {
CampaignChat(
@ -114,9 +121,10 @@ fun CampaignScreen(
}
},
leftPanel = {
PlayerRibbon(
CharacterRibbon(
modifier = Modifier.fillMaxHeight(),
viewModel = playerRibbonViewModel,
layoutDirection = LayoutDirection.Ltr,
onCharacterLeftClick = {
scope.launch {
playerDetailViewModel.showCharacter(
@ -139,8 +147,10 @@ fun CampaignScreen(
)
},
rightPanel = {
NpcRibbon(
CharacterRibbon(
modifier = Modifier.fillMaxHeight(),
viewModel = npcRibbonViewModel,
layoutDirection = LayoutDirection.Rtl,
onCharacterLeftClick = {
scope.launch {
npcDetailViewModel.showCharacter(
@ -166,11 +176,11 @@ fun CampaignScreen(
CharacterDetailPanel(
modifier = Modifier
.padding(all = 8.dp)
.padding(start = MaterialTheme.lwa.size.portrait.minimized.width + 8.dp)
.width(width = 128.dp * 4)
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr),
blurController = blurController,
detailPanelViewModel = npcDetailViewModel,
detailPanelViewModel = playerDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel,
@ -183,11 +193,11 @@ fun CampaignScreen(
CharacterDetailPanel(
modifier = Modifier
.padding(all = 8.dp)
.padding(end = MaterialTheme.lwa.size.portrait.minimized.width + 8.dp)
.width(width = 128.dp * 4)
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
blurController = blurController,
detailPanelViewModel = playerDetailViewModel,
detailPanelViewModel = npcDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel,
@ -308,6 +318,7 @@ private fun CampaignLayout(
) {
val density = LocalDensity.current
val mainState = remember { mutableStateOf(DpSize.Unspecified) }
val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val leftPanelState = remember { mutableStateOf(DpSize.Unspecified) }
val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
@ -315,6 +326,7 @@ private fun CampaignLayout(
val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val scope = remember {
CampaignLayoutScope(
main = mainState,
leftOverlay = leftOverlayState,
leftPanel = leftPanelState,
rightOverlay = rightOverlayState,
@ -335,14 +347,18 @@ private fun CampaignLayout(
) {
Box(
modifier = Modifier
.align(alignment = Alignment.Center)
.fillMaxSize(),
.onSizeChanged { mainState.value = it.toDp(density) }
.matchParentSize(),
) {
main()
}
Box(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.align(alignment = Alignment.BottomStart)
.padding(
start = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp,
end = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp + 56.dp,
)
.onSizeChanged { chatOverlayState.value = it.toDp(density) },
) {
chat()
@ -394,24 +410,31 @@ private fun CampaignKeyHandler(
onPlayerNumber: (index: Int) -> Unit,
onAltPLayerNumber: (index: Int) -> Unit,
) {
fun KeyEvent.callback(index: Int) {
if (isAltPressed) onAltPLayerNumber(index) else onPlayerNumber(index)
}
KeyHandler {
if (it.type != KeyEventType.KeyDown) return@KeyHandler false
if (it.type != KeyEventType.KeyDown) {
return@KeyHandler false
}
if (it.key == Key.Escape) {
onDismissRequest()
return@KeyHandler true
}
if (it.isCtrlPressed.not()) return@KeyHandler false
if (it.isCtrlPressed.not()) {
return@KeyHandler false
}
when (it.key) {
Key.Escape -> onDismissRequest()
Key.One, Key.NumPad1 -> if (it.isAltPressed) onAltPLayerNumber(0) else onPlayerNumber(0)
Key.Two, Key.NumPad2 -> if (it.isAltPressed) onAltPLayerNumber(1) else onPlayerNumber(1)
Key.Three, Key.NumPad3 -> if (it.isAltPressed) onAltPLayerNumber(2) else onPlayerNumber(2)
Key.Four, Key.NumPad4 -> if (it.isAltPressed) onAltPLayerNumber(3) else onPlayerNumber(3)
Key.Five, Key.NumPad5 -> if (it.isAltPressed) onAltPLayerNumber(4) else onPlayerNumber(4)
Key.Six, Key.NumPad6 -> if (it.isAltPressed) onAltPLayerNumber(5) else onPlayerNumber(5)
Key.Seven, Key.NumPad7 -> if (it.isAltPressed) onAltPLayerNumber(6) else onPlayerNumber(6)
Key.Eight, Key.NumPad8 -> if (it.isAltPressed) onAltPLayerNumber(7) else onPlayerNumber(7)
Key.Nine, Key.NumPad9 -> if (it.isAltPressed) onAltPLayerNumber(8) else onPlayerNumber(8)
Key.One, Key.NumPad1 -> it.callback(index = 0)
Key.Two, Key.NumPad2 -> it.callback(index = 1)
Key.Three, Key.NumPad3 -> it.callback(index = 2)
Key.Four, Key.NumPad4 -> it.callback(index = 3)
Key.Five, Key.NumPad5 -> it.callback(index = 4)
Key.Six, Key.NumPad6 -> it.callback(index = 5)
Key.Seven, Key.NumPad7 -> it.callback(index = 6)
Key.Eight, Key.NumPad8 -> it.callback(index = 7)
Key.Nine, Key.NumPad9 -> it.callback(index = 8)
else -> return@KeyHandler false
}
return@KeyHandler true
@ -427,6 +450,7 @@ private fun IntSize.toDp(density: Density) = with(density) {
@Stable
data class CampaignLayoutScope(
val main: State<DpSize>,
val leftOverlay: State<DpSize>,
val leftPanel: State<DpSize>,
val rightOverlay: State<DpSize>,

View file

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

View file

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

View file

@ -0,0 +1,70 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
@Composable
fun CharacterRibbon(
modifier: Modifier = Modifier,
layoutDirection: LayoutDirection,
viewModel: CharacterRibbonViewModel,
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection
) {
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
items = characters.value,
key = { it.characterSheetId },
) {
Row(
modifier = Modifier
.animateItem()
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f },
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Box {
CharacterRibbonPortrait(
character = it.portrait,
onCharacterLeftClick = { onCharacterLeftClick(it.characterSheetId) },
onCharacterRightClick = { onCharacterRightClick(it.characterSheetId) },
onLevelUp = { onLevelUp(it.characterSheetId) },
)
CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
)
}
CharacterRibbonAlteration(
status = it.status,
direction = LayoutDirection.Ltr,
)
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
@ -6,8 +6,6 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign
class NpcRibbonViewModel(

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
@ -6,8 +6,6 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign
class PlayerRibbonViewModel(

View file

@ -32,6 +32,7 @@ import com.pixelized.desktop.lwa.ui.theme.lwa
@Stable
data class CharacterRibbonAlterationUio(
val icon: String,
val tooltips: BasicTooltipUio?,
)
@ -44,25 +45,22 @@ fun CharacterRibbonAlteration(
direction: LayoutDirection,
status: List<List<CharacterRibbonAlterationUio>>,
) {
val currentDirection: LayoutDirection = LocalLayoutDirection.current
val currentDirection = LocalLayoutDirection.current
CompositionLocalProvider(
LocalLayoutDirection provides direction
) {
Row(
modifier = Modifier
.animateContentSize()
.size(size = size)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides currentDirection
) {
status.forEach { columns ->
Column(
modifier = Modifier.animateContentSize(),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
columns.forEach {
BasicTooltipLayout(
@ -71,12 +69,12 @@ fun CharacterRibbonAlteration(
tooltipPlacement = remember(currentDirection) {
TooltipPlacement.ComponentRect(
anchor = when (direction) {
LayoutDirection.Ltr -> Alignment.TopStart
LayoutDirection.Rtl -> Alignment.TopEnd
LayoutDirection.Ltr -> Alignment.CenterEnd
LayoutDirection.Rtl -> Alignment.CenterStart
},
alignment = when (direction) {
LayoutDirection.Ltr -> Alignment.BottomEnd
LayoutDirection.Rtl -> Alignment.BottomStart
LayoutDirection.Ltr -> Alignment.CenterEnd
LayoutDirection.Rtl -> Alignment.CenterStart
},
)
},
@ -103,4 +101,3 @@ fun CharacterRibbonAlteration(
}
}
}
}

View file

@ -1,64 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun NpcRibbon(
modifier: Modifier = Modifier,
viewModel: NpcRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
items = characters.value,
key = { it.characterSheetId },
) {
Row(
modifier = Modifier
.animateItem()
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f },
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
CharacterRibbonAlteration(
status = it.status,
direction = LayoutDirection.Rtl,
)
Box {
CharacterRibbonPortrait(
character = it.portrait,
onCharacterLeftClick = { onCharacterLeftClick(it.characterSheetId) },
onCharacterRightClick = { onCharacterRightClick(it.characterSheetId) },
onLevelUp = { onLevelUp(it.characterSheetId) },
)
CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
)
}
}
}
}
}

View file

@ -1,64 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun PlayerRibbon(
modifier: Modifier = Modifier,
viewModel: PlayerRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
items = characters.value,
key = { it.characterSheetId },
) {
Row(
modifier = Modifier
.animateItem()
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f },
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Box {
CharacterRibbonPortrait(
character = it.portrait,
onCharacterLeftClick = { onCharacterLeftClick(it.characterSheetId) },
onCharacterRightClick = { onCharacterRightClick(it.characterSheetId) },
onLevelUp = { onLevelUp(it.characterSheetId) },
)
CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
)
}
CharacterRibbonAlteration(
status = it.status,
direction = LayoutDirection.Ltr,
)
}
}
}
}

View file

@ -1,18 +1,28 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -21,13 +31,10 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
@ -47,21 +54,21 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessag
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_more_down_24dp
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel
@Stable
data class ChatSettingsUio(
val autoShowChat: Boolean,
val autoScrollChat: Boolean,
val autoHideChat: Boolean,
val show: Boolean,
val autoScroll: Boolean,
) {
companion object {
fun default() = with(SettingsUseCase().defaultSettings()) {
ChatSettingsUio(
autoShowChat = chat.autoShowChat,
autoScrollChat = chat.autoScrollChat,
autoHideChat = chat.autoHideChat,
show = chat.showChat,
autoScroll = chat.autoScrollChat,
)
}
}
@ -73,9 +80,9 @@ fun CampaignChat(
modifier: Modifier = Modifier,
chatViewModel: CampaignChatViewModel = koinViewModel(),
) {
val scope = rememberCoroutineScope()
val lazyState = rememberLazyListState()
val animatedChatWidth = rememberAnimatedChatWidth()
val campaignLayoutScope = LocalCampaignLayoutScope.current
val colorScheme = MaterialTheme.lwa.colorScheme
val messages = chatViewModel.messages.collectAsState()
val settings = chatViewModel.settings.collectAsState()
@ -84,34 +91,23 @@ fun CampaignChat(
lazyState = lazyState,
messages = messages,
settings = settings,
displayChat = chatViewModel::displayChat,
hideChat = chatViewModel::hideChat,
)
Box(
modifier = modifier
.size(
width = animatedChatWidth.value,
height = MaterialTheme.lwa.size.portrait.minimized.height * 2 + 8.dp,
)
.graphicsLayer {
alpha = chatViewModel.chatAnimatedVisibility.value
}
.background(
Row(
modifier = modifier.background(
shape = remember { RoundedCornerShape(8.dp) },
color = remember { colorScheme.elevated.base1dp.copy(alpha = 0.5f) },
)
.onPointerEvent(eventType = PointerEventType.Enter) {
scope.launch { chatViewModel.displayChat() }
}
.onPointerEvent(eventType = PointerEventType.Exit) {
if (settings.value.autoHideChat) {
scope.launch { chatViewModel.hideChat() }
}
},
),
) {
AnimatedVisibility(
visible = settings.value.show,
enter = fadeIn() + expandIn(),
exit = fadeOut() + shrinkOut(),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.width(width = campaignLayoutScope.chatOverlay.value.width - (32.dp + 8.dp))
.heightIn(min = MaterialTheme.lwa.size.portrait.minimized.height * 2 + 8.dp),
state = lazyState,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
@ -133,6 +129,26 @@ fun CampaignChat(
}
}
}
Column {
IconButton(
modifier = Modifier.size(size = 32.dp),
onClick = chatViewModel::toggleChat
) {
val rotation = animateFloatAsState(
targetValue = if (settings.value.show) 0f else 180f,
)
Icon(
modifier = Modifier
.size(size = 16.dp)
.graphicsLayer {
this.rotationZ = rotation.value
},
painter = painterResource(Res.drawable.ic_more_down_24dp),
contentDescription = null,
)
}
}
}
}
@Composable
@ -140,24 +156,16 @@ private fun ChatScrollDownEffect(
lazyState: LazyListState,
messages: State<List<TextMessage>>,
settings: State<ChatSettingsUio>,
displayChat: suspend () -> Unit,
hideChat: suspend () -> Unit,
) {
LaunchedEffect(
key1 = messages.value.lastOrNull()?.id,
) {
if (messages.value.isNotEmpty()) {
if (settings.value.autoShowChat) {
displayChat()
}
if (settings.value.autoScrollChat) {
if (settings.value.autoScroll) {
lazyState.animateScrollToItem(
index = messages.value.lastIndex + 1,
)
}
if (settings.value.autoHideChat) {
hideChat()
}
}
}
}
@ -167,9 +175,11 @@ private fun ChatScrollDownEffect(
private fun rememberAnimatedChatWidth(
campaignScreenScope: CampaignLayoutScope = LocalCampaignLayoutScope.current,
windowsState: WindowState = LocalWindowState.current,
settings: State<ChatSettingsUio>,
): State<Dp> {
val chatWidth = remember(windowsState, campaignScreenScope) {
derivedStateOf {
if (settings.value.show) {
val minChatWidth = 64.dp * 8
val maxChatWidth = 64.dp * 12
val windowWidth = windowsState.size.width
@ -179,7 +189,12 @@ private fun rememberAnimatedChatWidth(
} else {
minChatWidth
}
} else {
0.dp
}
}
return animateDpAsState(targetValue = chatWidth.value)
}
return animateDpAsState(
targetValue = chatWidth.value,
)
}

View file

@ -1,7 +1,5 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
@ -13,22 +11,20 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class CampaignChatViewModel(
private val settingsRepository: SettingsRepository,
private val campaignRepository: CampaignRepository,
campaignRepository: CampaignRepository,
networkRepository: NetworkRepository,
textMessageFactory: TextMessageFactory,
) : ViewModel() {
val chatAnimatedVisibility = Animatable(0f)
val settings = settingsRepository.settingsFlow().map {
ChatSettingsUio(
autoShowChat = it.chat.autoShowChat,
autoScrollChat = it.chat.autoScrollChat,
autoHideChat = it.chat.autoHideChat,
show = it.chat.showChat,
autoScroll = it.chat.autoScrollChat,
)
}.stateIn(
scope = viewModelScope,
@ -59,20 +55,14 @@ class CampaignChatViewModel(
initialValue = emptyList(),
)
suspend fun displayChat() {
chatAnimatedVisibility.animateTo(
targetValue = 1f,
)
}
suspend fun hideChat() {
fun toggleChat() {
viewModelScope.launch {
val settings = settingsRepository.settingsFlow().value
chatAnimatedVisibility.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 2000,
delayMillis = settings.chat.autoHideDelay * 1000,
settingsRepository.update(
settings = settings.copy(
chat = settings.chat.copy(showChat = settings.chat.showChat.not())
)
)
}
}
}

View file

@ -10,12 +10,13 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.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 kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_camping_24dp
@ -36,7 +37,8 @@ fun GMActionPage(
) {
val scope = rememberCoroutineScope()
val scroll = rememberScrollState()
val actions = viewModel.actions.collectAsState()
val actions = viewModel.actions.collectAsStateWithLifecycle()
val validationDialog = viewModel.validationDialog.collectAsStateWithLifecycle()
GMActionContent(
actions = actions,
@ -66,6 +68,10 @@ fun GMActionPage(
ErrorSnackHandler(
error = viewModel.error,
)
ConfirmationDialog(
dialog = validationDialog,
)
}
@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.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.composable.confirmation.ConfirmationDialogUio
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.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
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__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(
private val characterRepository: CharacterSheetRepository,
private val networkRepository: NetworkRepository,
private val campaignRepository: CampaignRepository,
private val actionUseCase: GMActionUseCase,
campaignRepository: CampaignRepository,
) : ViewModel() {
private val _error = MutableSharedFlow<ErrorSnackUio>()
@ -39,68 +52,102 @@ class GMActionViewModel(
initialValue = null,
)
private val _validationDialog = MutableStateFlow<ConfirmationDialogUio?>(null)
val validationDialog: StateFlow<ConfirmationDialogUio?> = _validationDialog
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 = {
try {
networkRepository.share(
GameAdminEvent.ServerSynchronization(
timestamp = System.currentTimeMillis(),
)
)
actionUseCase.invalidateServerCache()
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
}
},
onDismissRequest = {
_validationDialog.value = null
}
)
}
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) {
showConfirmationDialog(
title = Res.string.game_master__actions__party_heal__title,
description = Res.string.game_master__actions__party_heal__description,
onConfirmationRequest = {
try {
characterRepository.updateCharacter(
sheet = updated,
create = false,
)
actionUseCase.healPlayerParty()
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
}
}
}
}
suspend fun onNpcVisibility() {
try {
networkRepository.share(
GameMasterEvent.ToggleNpc(
timestamp = System.currentTimeMillis(),
},
)
)
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
}
}
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
},
description = when (actions.value?.party) {
true -> Res.string.game_master__actions__hide_player__description
else -> Res.string.game_master__actions__show_player__description
},
onConfirmationRequest = {
try {
networkRepository.share(
GameMasterEvent.TogglePlayer(
timestamp = System.currentTimeMillis(),
)
)
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)
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
blurController = blurController,
detailPanelViewModel = characterDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,

View file

@ -15,16 +15,8 @@ import lwacharactersheet.composeapp.generated.resources.ic_fan_focus_24dp
import lwacharactersheet.composeapp.generated.resources.ic_format_list_numbered_24dp
import lwacharactersheet.composeapp.generated.resources.ic_ifl_24dp
import lwacharactersheet.composeapp.generated.resources.ic_timer_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_hide_delay_description
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_hide_delay_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_hide_description
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_hide_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_scroll_description
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_scroll_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_show_description
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__auto_show_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__line_count_description
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__line_count_title
import lwacharactersheet.composeapp.generated.resources.settings__chat_log__section
@ -92,48 +84,6 @@ class SettingsViewModel(
SettingSectionUio(
title = Res.string.settings__chat_log__section,
),
SettingToggleItemUio(
icon = Res.drawable.ic_visibility_24dp,
title = Res.string.settings__chat_log__auto_show_title,
description = Res.string.settings__chat_log__auto_show_description,
checked = booleanStates.autoShowChat,
onToggle = {
settingsRepository.update(
settings = settings.value.copy(
chat = settings.value.chat.copy(autoShowChat = it)
)
)
},
),
SettingToggleItemUio(
icon = Res.drawable.ic_visibility_off_24dp,
title = Res.string.settings__chat_log__auto_hide_title,
description = Res.string.settings__chat_log__auto_hide_description,
checked = booleanStates.autoHideChat,
onToggle = {
settingsRepository.update(
settings = settings.value.copy(
chat = settings.value.chat.copy(autoHideChat = it)
)
)
},
),
SettingNumberItemUio(
icon = Res.drawable.ic_timer_24dp,
title = Res.string.settings__chat_log__auto_hide_delay_title,
description = Res.string.settings__chat_log__auto_hide_delay_description,
enable = booleanStates.autoHideChat,
value = intStates.autoHideDelay,
onValueChange = {
if (it in 0..999) {
settingsRepository.update(
settings = settings.value.copy(
chat = settings.value.chat.copy(autoHideDelay = it)
)
)
}
}
),
SettingToggleItemUio(
icon = Res.drawable.ic_fan_focus_24dp,
title = Res.string.settings__chat_log__auto_scroll_title,
@ -170,9 +120,6 @@ class SettingsViewModel(
settingsRepository.settingsFlow().collect { settings ->
booleanStates.dynamicDice.value = settings.portrait.dynamicDice
intStates.dynamicDiceDelay.value = settings.portrait.dynamicDiceDelay
booleanStates.autoShowChat.value = settings.chat.autoShowChat
booleanStates.autoHideChat.value = settings.chat.autoHideChat
intStates.autoHideDelay.value = settings.chat.autoHideDelay
booleanStates.autoScrollChat.value = settings.chat.autoScrollChat
intStates.maxLineCount.value = settings.chat.maxLineCount
}
@ -197,15 +144,6 @@ class SettingsViewModel(
private val HashMap<String, MutableState<Int>>.dynamicDiceDelay
get() = getOrPut("DYNAMIC_DICE_DELAY") { mutableStateOf(settings.value.portrait.dynamicDiceDelay) }
private val HashMap<String, MutableState<Boolean>>.autoShowChat
get() = getOrPut("AUTO_SHOW_CHAT") { mutableStateOf(settings.value.chat.autoShowChat) }
private val HashMap<String, MutableState<Boolean>>.autoHideChat
get() = getOrPut("AUTO_HIDE_CHAT") { mutableStateOf(settings.value.chat.autoHideChat) }
private val HashMap<String, MutableState<Int>>.autoHideDelay
get() = getOrPut("AUTO_HIDE_DELAY") { mutableStateOf(settings.value.chat.autoHideDelay) }
private val HashMap<String, MutableState<Boolean>>.autoScrollChat
get() = getOrPut("AUTO_SCROLL_CHAT") { mutableStateOf(settings.value.chat.autoScrollChat) }

View file

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

View file

@ -8,12 +8,10 @@ class SettingsUseCase {
playerName = "",
portrait = Settings.Portrait(
dynamicDice = true,
dynamicDiceDelay = 5000,
dynamicDiceDelay = 8000,
),
chat = Settings.Chat(
autoHideChat = true,
autoHideDelay = 8,
autoShowChat = true,
showChat = true,
autoScrollChat = true,
maxLineCount = 200,
),

View file

@ -10,6 +10,7 @@ koin = "4.0.0"
turtle = "0.10.0"
logback = "1.5.17"
coil = "3.1.0"
zoomable = "2.7.0"
ui-graphics-android = "1.7.8"
buildkonfig = "0.17.0"
@ -35,6 +36,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-
# UI.
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
engawapg-zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
# Injection with Koin
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }