Add the edit & create character to the gamemaster screen.

This commit is contained in:
Thomas Andres Gomez 2025-03-16 17:47:38 +01:00
parent 662e270f3f
commit 2056348ec0
30 changed files with 420 additions and 143 deletions

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:pathData="M360,570q-21,0 -35.5,-14.5T310,520q0,-21 14.5,-35.5T360,470q21,0 35.5,14.5T410,520q0,21 -14.5,35.5T360,570ZM600,570q-21,0 -35.5,-14.5T550,520q0,-21 14.5,-35.5T600,470q21,0 35.5,14.5T650,520q0,21 -14.5,35.5T600,570ZM480,800q134,0 227,-93t93,-227q0,-24 -3,-46.5T786,390q-21,5 -42,7.5t-44,2.5q-91,0 -172,-39T390,252q-32,78 -91.5,135.5T160,474v6q0,134 93,227t227,93ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM426,165q42,70 114,112.5T700,320q14,0 27,-1.5t27,-3.5q-42,-70 -114,-112.5T480,160q-14,0 -27,1.5t-27,3.5ZM177,379q51,-29 89,-75t57,-103q-51,29 -89,75t-57,103ZM426,165ZM323,201Z"
android:fillColor="#5f6368"/>
</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:pathData="m814,884 l-88,-88q-51,39 -113,61.5T482,880q-82,0 -155,-31.5t-127.5,-86Q145,708 113.5,635T82,480q0,-69 22.5,-131.5T166,235l-88,-87 56,-56 736,736 -56,56ZM831,675 L772,616q14,-32 22,-66t8,-70q0,-24 -3,-46.5T788,390q-21,5 -42,7.5t-44,2.5q-56,0 -106.5,-14.5T500,344L286,130q44,-24 93,-37t103,-13q83,0 155.5,31.5t127,85.5q54.5,54 86,127T882,480q0,53 -12.5,101.5T831,675ZM590,240ZM179,379q21,-12 39,-26.5t35,-31.5l-30,-29q-14,20 -25,41.5T179,379ZM426,165q42,70 114,112.5T700,320q14,0 27,-1.5t27,-3.5q-42,-70 -114,-112.5T480,160q-14,0 -27,1.5t-27,3.5ZM216,335ZM362,570q-21,0 -35.5,-14.5T312,520q0,-21 14.5,-35.5T362,470q21,0 35.5,14.5T412,520q0,21 -14.5,35.5T362,570ZM482,800q53,0 100,-15.5t86,-44.5L309,379q-30,32 -67.5,56T162,474v6q0,133 93.5,226.5T482,800Z"
android:fillColor="#5f6368"/>
</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:pathData="M40,800v-112q0,-34 17.5,-62.5T104,582q62,-31 126,-46.5T360,520q66,0 130,15.5T616,582q29,15 46.5,43.5T680,688v112L40,800ZM760,800v-120q0,-44 -24.5,-84.5T666,526q51,6 96,20.5t84,35.5q36,20 55,44.5t19,53.5v120L760,800ZM360,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM760,320q0,66 -47,113t-113,47q-11,0 -28,-2.5t-28,-5.5q27,-32 41.5,-71t14.5,-81q0,-42 -14.5,-81T544,168q14,-5 28,-6.5t28,-1.5q66,0 113,47t47,113ZM120,720h480v-32q0,-11 -5.5,-20T580,654q-54,-27 -109,-40.5T360,600q-56,0 -111,13.5T140,654q-9,5 -14.5,14t-5.5,20v32ZM360,400q33,0 56.5,-23.5T440,320q0,-33 -23.5,-56.5T360,240q-33,0 -56.5,23.5T280,320q0,33 23.5,56.5T360,400ZM360,720ZM360,320Z"
android:fillColor="#5f6368"/>
</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:pathData="M819,932 L680,793v7L40,800v-112q0,-34 17.5,-62.5T104,582q62,-31 126,-46.5T360,520q12,0 24.5,0.5T409,522l-42,-42h-7q-66,0 -113,-47t-47,-113v-7L27,140l57,-57L876,875l-57,57ZM666,526q51,6 96,20.5t84,35.5q36,20 55,44.5t19,53.5v120h-5L755,640q-9,-33 -31.5,-62.5T666,526ZM360,600q-56,0 -111,13.5T140,654q-9,5 -14.5,14t-5.5,20v32h480v-7l-87,-87q-38,-13 -76.5,-19.5T360,600ZM562,447q19,-28 28.5,-60t9.5,-67q0,-42 -14.5,-81T544,168q14,-5 28,-6.5t28,-1.5q66,0 113,47t47,113q0,66 -49.5,113T595,480l-33,-33ZM504,389 L440,325v-5q0,-33 -23.5,-56.5T360,240h-5l-64,-64q16,-8 33,-12t36,-4q66,0 113,47t47,113q0,19 -4,36t-12,33ZM365,720ZM398,282Z"
android:fillColor="#5f6368"/>
</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:pathData="M200,840q-33,0 -56.5,-23.5T120,760v-560q0,-33 23.5,-56.5T200,120h560q33,0 56.5,23.5T840,200v560q0,33 -23.5,56.5T760,840L200,840ZM200,760h560v-560L200,200v560ZM240,680h480L570,480 450,640l-90,-120 -120,160ZM200,760v-560,560ZM340,400q25,0 42.5,-17.5T400,340q0,-25 -17.5,-42.5T340,280q-25,0 -42.5,17.5T280,340q0,25 17.5,42.5T340,400Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -39,6 +39,11 @@ class DataSyncViewModel(
.onEach { campaignRepository.update() }
.launchIn(this)
networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED }
.onEach { characterRepository.updateCharacterPreviews() }
.launchIn(this)
networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED }
.combine(campaignRepository.campaignFlow) { _, campaign: Campaign -> campaign }

View file

@ -16,8 +16,13 @@ class CharacterSheetRepository(
private val scope = CoroutineScope(Dispatchers.IO + Job())
val characterSheetPreviewFlow get() = store.previewFlow
val characterDetailFlow get() = store.detailFlow
suspend fun updateCharacterPreviews() {
store.updateCharacterPreviews()
}
fun characterPreview(characterId: String?): CharacterSheetPreview? {
return characterSheetPreviewFlow.value.firstOrNull { it.characterSheetId == characterId }
}

View file

@ -32,7 +32,7 @@ class CharacterSheetStore(
val scope = CoroutineScope(Dispatchers.IO + Job())
// initial data loading.
scope.launch {
_previewFlow.value = charactersPreview()
updateCharacterPreviews()
}
// data update through WebSocket.
scope.launch {
@ -42,6 +42,10 @@ class CharacterSheetStore(
// region Rest
suspend fun updateCharacterPreviews() {
_previewFlow.value = charactersPreview()
}
suspend fun charactersPreview(): List<CharacterSheetPreview> {
val request = client.characters()
val data = request.map {

View file

@ -21,6 +21,7 @@ class SettingsFactory(
autoHideDelay = settings.autoHideDelay,
autoShowChat = settings.autoShowChat,
autoScrollChat = settings.autoScrollChat,
isAdmin = settings.isAdmin,
isGM = settings.isGM,
)
}
@ -46,6 +47,7 @@ class SettingsFactory(
autoHideDelay = json.autoHideDelay ?: default.autoHideDelay,
autoShowChat = json.autoShowChat ?: default.autoShowChat,
autoScrollChat = json.autoScrollChat ?: default.autoScrollChat,
isAdmin = json.isAdmin ?: default.isAdmin,
isGM = json.isGM ?: default.isGM,
)
}

View file

@ -9,6 +9,7 @@ data class Settings(
val autoHideDelay: Int,
val autoShowChat: Boolean,
val autoScrollChat: Boolean,
val isAdmin: Boolean,
val isGM: Boolean,
) {
val root: String get() = "http://${"${host}:${port}".removePrefix("http://")}"

View file

@ -13,4 +13,5 @@ data class SettingsJsonV1(
val autoShowChat: Boolean?,
val autoScrollChat: Boolean?,
val isGM: Boolean?,
val isAdmin: Boolean?,
) : SettingsJson

View file

@ -17,7 +17,7 @@ class CharacterSheetEditWindow(
fun WindowController.navigateToCharacterSheetEdit(
characterId: String?,
title: String,
title: String = "",
) {
showWindow(
window = CharacterSheetEditWindow(
@ -25,7 +25,7 @@ fun WindowController.navigateToCharacterSheetEdit(
title = title,
size = DpSize(
width = 600.dp,
height = maxWindowHeight,
height = maxWindowHeight - 32.dp,
),
)
)

View file

@ -18,7 +18,7 @@ class CharacterSheetWindow(
fun WindowController.navigateToCharacterSheet(
characterId: Campaign.CharacterInstance.Id,
title: String,
title: String = "Feuille de personnage",
) {
showWindow(
window = CharacterSheetWindow(
@ -26,7 +26,7 @@ fun WindowController.navigateToCharacterSheet(
title = title,
size = DpSize(
width = 400.dp + 64.dp,
height = maxWindowHeight,
height = maxWindowHeight - 32.dp,
),
)
)

View file

@ -22,7 +22,7 @@ fun WindowController.navigateToRollHistory(
title = title,
size = DpSize(
width = 400.dp + 64.dp,
height = maxWindowHeight,
height = maxWindowHeight - 32.dp,
)
)
)

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@ -60,6 +61,7 @@ object CharacterPortraitDefault {
@Stable
data class CharacterPortraitUio(
val id: Campaign.CharacterInstance.Id,
val idLabel: String?,
val portrait: String?,
val name: String,
val levelUp: Boolean,
@ -79,6 +81,7 @@ data class CharacterPortraitUio(
fun CharacterPortrait(
modifier: Modifier = Modifier,
size: DpSize = CharacterPortraitDefault.size,
levelUpOffset: Dp = 9.dp,
character: CharacterPortraitUio,
onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit,
onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit,
@ -106,8 +109,18 @@ fun CharacterPortrait(
)
}
character.idLabel?.let { label ->
Text(
modifier = Modifier.padding(start = 4.dp, top = 3.dp),
style = MaterialTheme.lwa.typography.portrait.idLabel,
text = label,
)
}
AnimatedVisibility(
modifier = Modifier.offset(x = (-8).dp, y = (-8).dp),
modifier = Modifier
.align(alignment = Alignment.TopEnd)
.offset(x = levelUpOffset, y = -levelUpOffset),
visible = character.levelUp,
enter = fadeIn(),
exit = fadeOut(),
@ -149,14 +162,12 @@ fun CharacterPortrait(
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Bold,
style = MaterialTheme.lwa.typography.portrait.value,
text = "${stats.hp}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Light,
style = MaterialTheme.lwa.typography.portrait.max,
text = "/${stats.maxHp}",
)
}
@ -171,14 +182,12 @@ fun CharacterPortrait(
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Bold,
style = MaterialTheme.lwa.typography.portrait.value,
text = "${stats.pp}",
)
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.caption,
style = MaterialTheme.lwa.typography.portrait.max,
text = "/${stats.maxPp}",
)
}

View file

@ -15,8 +15,9 @@ class CharacterRibbonFactory(
alterations: Map<String, List<FieldAlteration>>,
characterInstanceId: Campaign.CharacterInstance.Id,
characterInstance: Campaign.CharacterInstance,
enableDetail: Boolean,
displayCharacterStats: Boolean,
enableCharacterId: Boolean,
enableCharacterSheet: Boolean,
enableCharacterStats: Boolean,
): CharacterPortraitUio? {
if (characterSheet == null) return null
@ -27,11 +28,12 @@ class CharacterRibbonFactory(
return CharacterPortraitUio(
id = characterInstanceId,
idLabel = takeIf { enableCharacterId }?.let { characterInstanceId.instanceId.toString() },
portrait = alteredCharacterSheet.thumbnail,
name = alteredCharacterSheet.name,
levelUp = alteredCharacterSheet.shouldLevelUp,
enableDetail = enableDetail,
stats = takeIf { displayCharacterStats }?.let {
enableDetail = enableCharacterSheet,
stats = takeIf { enableCharacterStats }?.let {
CharacterPortraitUio.StatsDetail(
hp = alteredCharacterSheet.maxHp - characterInstance.damage,
maxHp = alteredCharacterSheet.maxHp,

View file

@ -41,6 +41,9 @@ abstract class CharacterRibbonViewModel(
abstract val Campaign.data: Map<CharacterInstance.Id, CharacterInstance>
abstract val enableCharacterSheet: Boolean
abstract val enableCharacterStats: Boolean
/**
* This flow is a tad complex so there is an explanation of wtf it's about :
* On a campaign update it go through every element of the abstract [data] map and either:
@ -58,14 +61,16 @@ abstract class CharacterRibbonViewModel(
combine(
characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId),
alterationRepository.alterationsFlow(characterInstanceId = entry.key),
) { sheet, alterations ->
settingsRepository.settingsFlow(),
) { sheet, alterations, settings ->
ribbonFactory.convertToPlayerPortraitUio(
characterSheet = sheet,
alterations = alterations,
characterInstanceId = entry.key,
characterInstance = entry.value,
enableDetail = settingsRepository.settings().isGM,
displayCharacterStats = settingsRepository.settings().isGM,
enableCharacterId = settings.isGM,
enableCharacterSheet = enableCharacterSheet || settings.isGM,
enableCharacterStats = enableCharacterStats || settings.isGM,
)
}
},

View file

@ -34,7 +34,9 @@ fun NpcRibbon(
items = characters.value,
key = { it.id },
) {
Row {
Row(
modifier = Modifier.animateItem(),
) {
CharacterPortraitRoll(
size = CharacterPortraitDefault.size,
value = viewModel.roll(characterId = it.id).value,

View file

@ -25,4 +25,7 @@ class NpcRibbonViewModel(
ribbonFactory = ribbonFactory,
) {
override val Campaign.data get() = npcs
override val enableCharacterSheet = false
override val enableCharacterStats = false
}

View file

@ -33,7 +33,9 @@ fun PlayerRibbon(
items = characters.value,
key = { it.id },
) {
Row {
Row(
modifier = Modifier.animateItem(),
) {
CharacterPortrait(
character = it,
onCharacter = onCharacter,

View file

@ -25,4 +25,7 @@ class PlayerRibbonViewModel(
ribbonFactory = ribbonFactory,
) {
override val Campaign.data get() = characters
override val enableCharacterSheet = true
override val enableCharacterStats = true
}

View file

@ -5,7 +5,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
@ -22,7 +21,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
@ -57,15 +55,15 @@ fun CampaignToolbar(
val title = viewModel.title.collectAsState()
val status = viewModel.status.collectAsState()
val isGM = viewModel.isGM.collectAsState()
val isAdmin = viewModel.isAdmin.collectAsState()
CampaignToolbarContent(
title = title,
status = status,
isGM = isGM,
isAdmin = isAdmin,
isNetworkMenuOpen = isNetworkMenuOpen,
isOverflowMenuOpen = isOverflowMenuOpen,
onGM = {
onAdmin = {
windows.navigateToGameMasterWindow()
},
onNetwork = {
@ -96,10 +94,10 @@ private fun CampaignToolbarContent(
modifier: Modifier = Modifier,
title: State<String>,
status: State<NetworkRepository.Status>,
isGM: State<Boolean>,
isAdmin: State<Boolean>,
isNetworkMenuOpen: State<Boolean>,
isOverflowMenuOpen: State<Boolean>,
onGM: () -> Unit,
onAdmin: () -> Unit,
onNetwork: () -> Unit,
onOverflow: () -> Unit,
onRollHistory: () -> Unit,
@ -116,17 +114,16 @@ private fun CampaignToolbarContent(
},
actions = {
AnimatedVisibility(
visible = isGM.value,
visible = isAdmin.value,
enter = fadeIn(),
exit = fadeOut(),
) {
TextButton(
modifier = Modifier.size(size = 48.dp).clip(shape = CircleShape),
onClick = onGM,
onClick = onAdmin,
) {
Text(
fontWeight = FontWeight.SemiBold,
text = "GM",
text = "Admin",
)
}
}

View file

@ -25,8 +25,8 @@ class CampaignToolbarViewModel(
initialValue = "",
)
val isGM = settingsRepository.settingsFlow()
.map { it.isGM }
val isAdmin = settingsRepository.settingsFlow()
.map { it.isAdmin }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,

View file

@ -1,7 +1,7 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio.Action
class GameMasterActionUseCase(
private val campaignRepository: CampaignRepository,

View file

@ -1,8 +1,8 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio
import com.pixelized.shared.lwa.model.campaign.Campaign
import lwacharactersheet.composeapp.generated.resources.Res
@ -18,7 +18,7 @@ class GameMasterFactory {
characters: List<CharacterSheetPreview>,
filter: String,
tags: Map<GMTagUio.TagId, Boolean>,
): List<GMCharacterPreviewUio> {
): List<GMCharacterUio> {
val normalizedFilter = Normalizer.normalize(filter, Normalizer.Form.NFD)
return characters.mapNotNull {
@ -36,7 +36,7 @@ class GameMasterFactory {
character: CharacterSheetPreview,
filter: String,
tags: Map<GMTagUio.TagId, Boolean>,
): GMCharacterPreviewUio? {
): GMCharacterUio? {
// get the characterInstanceId from the player list corresponding to this CharacterSheet if any
val characterInstanceId: Campaign.CharacterInstance.Id? =
campaign.characters.keys.firstOrNull {
@ -106,9 +106,10 @@ class GameMasterFactory {
)
}
// return the cell UIO.
return GMCharacterPreviewUio(
return GMCharacterUio(
characterSheetId = character.characterSheetId,
name = character.name, level = character.level,
name = character.name,
level = character.level,
tags = previewTagsList,
actions = actions,
)

View file

@ -3,12 +3,13 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.gestures.scrollBy
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
@ -19,30 +20,40 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreview
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacter
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTag
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__create__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__edit__title
import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel
@ -50,6 +61,9 @@ import org.koin.compose.viewmodel.koinViewModel
fun GameMasterScreen(
viewModel: GameMasterViewModel = koinViewModel(),
) {
val windows = LocalWindowController.current
val scope = rememberCoroutineScope()
val characters = viewModel.characters.collectAsState()
val tags = viewModel.tags.collectAsState()
@ -59,10 +73,27 @@ fun GameMasterScreen(
GameMasterContent(
modifier = Modifier.fillMaxSize(),
filter = viewModel.filter,
onGameMaster = viewModel::onGameMaster,
tags = tags,
characters = characters,
onTag = viewModel::onTag,
onCharacterAction = viewModel::onCharacterAction,
onCharacterSheetEdit = { characterSheetId ->
scope.launch {
windows.navigateToCharacterSheetEdit(
characterId = characterSheetId,
title = getString(Res.string.character_sheet_edit__edit__title),
)
}
},
onCharacterSheetCreate = {
scope.launch {
windows.navigateToCharacterSheetEdit(
characterId = null,
title = getString(Res.string.character_sheet_edit__create__title),
)
}
},
)
}
}
@ -73,9 +104,12 @@ private fun GameMasterContent(
filterChipsState: LazyListState = rememberLazyListState(),
filter: LwaTextFieldUio,
tags: State<List<GMTagUio>>,
characters: State<List<GMCharacterPreviewUio>>,
characters: State<List<GMCharacterUio>>,
onGameMaster: () -> Unit,
onTag: (GMTagUio.TagId) -> Unit,
onCharacterAction: (String, GMCharacterPreviewUio.Action) -> Unit,
onCharacterAction: (String, GMCharacterUio.Action) -> Unit,
onCharacterSheetEdit: (String) -> Unit,
onCharacterSheetCreate: () -> Unit,
) {
val scope = rememberCoroutineScope()
@ -87,6 +121,13 @@ private fun GameMasterContent(
Text(
text = "",
)
},
actions = {
TextButton(
onClick = onGameMaster,
) {
Text(text = "GameMaster")
}
}
)
},
@ -94,65 +135,105 @@ private fun GameMasterContent(
Column(
modifier = Modifier.padding(paddingValues = paddingValues)
) {
LwaTextField(
modifier = Modifier.fillMaxWidth(),
field = filter,
trailingIcon = {
val value = filter.valueFlow.collectAsState()
AnimatedVisibility(
visible = value.value.isNotBlank(),
enter = fadeIn(),
exit = fadeOut(),
Surface(
elevation = 1.dp,
) {
Column {
LwaTextField(
modifier = Modifier.fillMaxWidth(),
field = filter,
trailingIcon = {
val value = filter.valueFlow.collectAsState()
AnimatedVisibility(
visible = value.value.isNotBlank(),
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = { filter.onValueChange.invoke("") },
) {
Icon(
painter = painterResource(Res.drawable.ic_cancel_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
}
)
LazyRow(
modifier = Modifier.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
scope.launch {
filterChipsState.scrollBy(-delta)
}
},
),
state = filterChipsState,
contentPadding = remember { PaddingValues(all = 8.dp) },
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
IconButton(
onClick = { filter.onValueChange.invoke("") },
) {
Icon(
painter = painterResource(Res.drawable.ic_cancel_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
items(
items = tags.value,
) { tag ->
GMTag(
style = MaterialTheme.lwa.typography.base.body1,
tag = tag,
onTag = { onTag(tag.id) },
)
}
}
}
)
LazyRow(
modifier = Modifier.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
scope.launch {
filterChipsState.scrollBy(-delta)
}
},
),
state = filterChipsState,
contentPadding = remember { PaddingValues(all = 8.dp) },
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
items(
items = tags.value,
) { tag ->
GMTag(
style = MaterialTheme.lwa.typography.base.body1,
tag = tag,
onTag = { onTag(tag.id) },
)
}
}
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = remember { PaddingValues(all = 8.dp) },
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
) {
items(
items = characters.value,
) { character ->
GMCharacterPreview(
modifier = Modifier.fillMaxWidth(),
character = character,
onAction = { action ->
onCharacterAction(character.characterSheetId, action)
}
LazyColumn(
modifier = Modifier.matchParentSize(),
contentPadding = remember {
PaddingValues(
start = 8.dp,
top = 8.dp,
end = 8.dp,
bottom = 8.dp + 48.dp + 8.dp,
)
},
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
items(
items = characters.value,
key = { it.characterSheetId },
) { character ->
GMCharacter(
modifier = Modifier
.fillMaxWidth()
.animateItem(),
character = character,
onEdit = {
onCharacterSheetEdit(character.characterSheetId)
},
onAction = { action ->
onCharacterAction(character.characterSheetId, action)
},
)
}
}
IconButton(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(all = 8.dp)
.background(
color = MaterialTheme.lwa.colorScheme.base.primary,
shape = CircleShape,
),
onClick = onCharacterSheetCreate,
) {
Icon(
imageVector = Icons.Default.Add,
tint = MaterialTheme.lwa.colorScheme.base.onPrimary,
contentDescription = null,
)
}
}

View file

@ -4,8 +4,9 @@ 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.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio.TagId
import kotlinx.coroutines.flow.MutableStateFlow
@ -20,9 +21,10 @@ import lwacharactersheet.composeapp.generated.resources.game_master__character_t
import org.jetbrains.compose.resources.getString
class GameMasterViewModel(
private val campaignRepository: CampaignRepository,
private val characterSheetRepository: CharacterSheetRepository,
private val gameMasterFactory: GameMasterFactory,
campaignRepository: CampaignRepository,
characterSheetRepository: CharacterSheetRepository,
private val settingsRepository: SettingsRepository,
private val factory: GameMasterFactory,
private val useCase: GameMasterActionUseCase,
) : ViewModel() {
@ -63,16 +65,25 @@ class GameMasterViewModel(
characterSheetRepository.characterSheetPreviewFlow,
filter.valueFlow,
_tags,
gameMasterFactory::convertToGMCharacterPreviewUio,
factory::convertToGMCharacterPreviewUio,
).stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
fun onGameMaster() {
val settings = settingsRepository.settings()
settingsRepository.update(
settings = settings.copy(
isGM = settings.isGM.not(),
)
)
}
fun onCharacterAction(
characterSheetId: String,
action: GMCharacterPreviewUio.Action,
action: GMCharacterUio.Action,
) {
viewModelScope.launch {
try {

View file

@ -16,6 +16,7 @@ import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@ -27,7 +28,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio.Action
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__add_to_group
@ -36,10 +37,17 @@ import lwacharactersheet.composeapp.generated.resources.game_master__character_a
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__remove_from_group
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__remove_from_npc
import lwacharactersheet.composeapp.generated.resources.game_master__character_level__label
import lwacharactersheet.composeapp.generated.resources.ic_face_24dp
import lwacharactersheet.composeapp.generated.resources.ic_face_retouching_off_24dp
import lwacharactersheet.composeapp.generated.resources.ic_group_24dp
import lwacharactersheet.composeapp.generated.resources.ic_group_off_24dp
import lwacharactersheet.composeapp.generated.resources.ic_imagesmode_24dp
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable
data class GMCharacterPreviewUio(
data class GMCharacterUio(
val characterSheetId: String,
val name: String,
val level: Int,
@ -47,21 +55,33 @@ data class GMCharacterPreviewUio(
val actions: List<Action>,
) {
@Stable
sealed class Action {
sealed class Action(
val icon: DrawableResource,
) {
@Stable
data object DisplayPortrait : Action()
data object DisplayPortrait : Action(
icon = Res.drawable.ic_imagesmode_24dp,
)
@Stable
data object AddToGroup : Action()
data object AddToGroup : Action(
icon = Res.drawable.ic_group_24dp,
)
@Stable
data class RemoveFromGroup(val instanceId: Int) : Action()
data class RemoveFromGroup(val instanceId: Int) : Action(
icon = Res.drawable.ic_group_off_24dp,
)
@Stable
data object AddToNpc : Action()
data object AddToNpc : Action(
icon = Res.drawable.ic_face_24dp,
)
@Stable
data class RemoveFromNpc(val instanceId: Int) : Action()
data class RemoveFromNpc(val instanceId: Int) : Action(
icon = Res.drawable.ic_face_retouching_off_24dp,
)
}
}
@ -70,10 +90,11 @@ object GMCharacterPreviewDefault {
}
@Composable
fun GMCharacterPreview(
fun GMCharacter(
modifier: Modifier = Modifier,
padding: PaddingValues = GMCharacterPreviewDefault.padding,
character: GMCharacterPreviewUio,
character: GMCharacterUio,
onEdit: () -> Unit,
onAction: (Action) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
@ -108,20 +129,22 @@ fun GMCharacterPreview(
),
)
}
OverflowActionMenu(
MenuActions(
character = character,
onEdit = onEdit,
onAction = onAction,
)
}
Row(
modifier = Modifier
.padding(paddingValues = padding)
.padding(bottom = 16.dp),
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
character.tags.forEach { tag ->
GMTag(
style = MaterialTheme.lwa.typography.base.caption,
elevation = 4.dp,
tag = tag,
)
}
@ -130,10 +153,36 @@ fun GMCharacterPreview(
}
}
@Composable
private fun MenuActions(
modifier: Modifier = Modifier,
character: GMCharacterUio,
onEdit: () -> Unit,
onAction: (Action) -> Unit,
) {
Row(
modifier = modifier,
) {
IconButton(
onClick = onEdit
) {
Icon(
imageVector = Icons.Default.Edit,
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
OverflowActionMenu(
character = character,
onAction = onAction,
)
}
}
@Composable
private fun OverflowActionMenu(
modifier: Modifier = Modifier,
character: GMCharacterPreviewUio,
character: GMCharacterUio,
onAction: (Action) -> Unit,
) {
val overflowMenu = remember(character) {
@ -165,33 +214,43 @@ private fun OverflowActionMenu(
onAction(action)
},
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = when (action) {
Action.DisplayPortrait -> stringResource(
Res.string.game_master__character_action__display_portrait,
)
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(action.icon),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
style = MaterialTheme.lwa.typography.base.body1,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = when (action) {
Action.DisplayPortrait -> stringResource(
Res.string.game_master__character_action__display_portrait,
)
Action.AddToGroup -> stringResource(
Res.string.game_master__character_action__add_to_group,
)
Action.AddToGroup -> stringResource(
Res.string.game_master__character_action__add_to_group,
)
Action.AddToNpc -> stringResource(
Res.string.game_master__character_action__add_to_npc,
)
Action.AddToNpc -> stringResource(
Res.string.game_master__character_action__add_to_npc,
)
is Action.RemoveFromGroup -> stringResource(
Res.string.game_master__character_action__remove_from_group,
action.instanceId,
)
is Action.RemoveFromGroup -> stringResource(
Res.string.game_master__character_action__remove_from_group,
action.instanceId,
)
is Action.RemoveFromNpc -> stringResource(
Res.string.game_master__character_action__remove_from_npc,
action.instanceId,
)
}
)
is Action.RemoveFromNpc -> stringResource(
Res.string.game_master__character_action__remove_from_npc,
action.instanceId,
)
}
)
}
}
}
},

View file

@ -4,12 +4,16 @@ import androidx.compose.material.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.pixelized.desktop.lwa.ui.theme.color.LwaColorPalette
import com.pixelized.desktop.lwa.ui.theme.color.LwaColors
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography.Settings
import lwacharactersheet.composeapp.generated.resources.Res
@ -22,6 +26,7 @@ data class LwaTypography(
val base: Typography,
val chat: Chat,
val settings: Settings,
val portrait: Portrait,
) {
@Stable
data class Chat(
@ -36,6 +41,13 @@ data class LwaTypography(
val input: TextStyle,
val description: TextStyle,
)
@Stable
data class Portrait(
val idLabel: TextStyle,
val value: TextStyle,
val max: TextStyle,
)
}
@Composable
@ -90,6 +102,32 @@ fun lwaTypography(
fontStyle = FontStyle.Italic,
color = colors.base.onSurface.copy(alpha = 0.7f),
),
),
portrait = LwaTypography.Portrait(
idLabel = base.caption.copy(
fontWeight = FontWeight.SemiBold,
shadow = Shadow(
color = Color.Black,
offset = Offset(x = 1f, y = 1f),
blurRadius = 2f,
),
),
value = base.caption.copy(
fontWeight = FontWeight.Bold,
shadow = Shadow(
color = Color.Black,
offset = Offset(x = 1f, y = 1f),
blurRadius = 2f,
),
),
max = base.caption.copy(
fontWeight = FontWeight.Light,
shadow = Shadow(
color = Color.Black,
offset = Offset(x = 1f, y = 1f),
blurRadius = 2f,
),
),
)
)
}

View file

@ -13,6 +13,7 @@ class SettingsUseCase {
autoHideDelay = 8,
autoShowChat = true,
autoScrollChat = true,
isAdmin = false,
isGM = false,
)