diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 4251b6a..39eea3a 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -178,10 +178,10 @@ niv : %1$d - niv: %1$d - joueur - joueur: %1$d - npc - npc: %1$d + Joueur + Joueur-%1$d + Npc + Npc-%1$d Afficher le portrait Ajouter au groupe Retirer du groupe (id: %1$d) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterFactory.kt index ddca343..505328a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterFactory.kt @@ -3,12 +3,11 @@ 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.GMTagUio import com.pixelized.shared.lwa.model.campaign.Campaign import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character_label -import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character_search import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_label -import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_search import org.jetbrains.compose.resources.getString import java.text.Normalizer @@ -18,6 +17,7 @@ class GameMasterFactory { campaign: Campaign, characters: List, filter: String, + tags: Map, ): List { val normalizedFilter = Normalizer.normalize(filter, Normalizer.Form.NFD) @@ -26,6 +26,7 @@ class GameMasterFactory { campaign = campaign, character = it, filter = normalizedFilter, + tags = tags, ) } } @@ -34,71 +35,67 @@ class GameMasterFactory { campaign: Campaign, character: CharacterSheetPreview, filter: String, + tags: Map, ): GMCharacterPreviewUio? { - val characterId = campaign.characters.keys.firstOrNull { - it.characterSheetId == character.characterSheetId - } - + // get the characterInstanceId from the player list corresponding to this CharacterSheet if any + val characterInstanceId: Campaign.CharacterInstance.Id? = + campaign.characters.keys.firstOrNull { + it.characterSheetId == character.characterSheetId + } + // get all characterInstanceId from the npcs list corresponding to this CharacterSheet if any val npcIds = campaign.npcs.keys.filter { it.characterSheetId == character.characterSheetId } - - var playerTagHighlighted = false - var npcTagHighlighted = false - - // Filter process. + // Filter process : Name. if (filter.isNotEmpty()) { val normalizedName = Normalizer.normalize(character.name, Normalizer.Form.NFD) // If the filter is not empty and the character is not - val playerTag = getString(Res.string.game_master__character_tag__character_search) - val npcTag = getString(Res.string.game_master__character_tag__npc_search) - - playerTagHighlighted = playerTag.contains(other = filter, ignoreCase = true) - if (playerTagHighlighted && characterId == null) { - return null - } - - npcTagHighlighted = npcTag.contains(other = filter, ignoreCase = true) - if (npcTagHighlighted && npcIds.isEmpty()) { - return null - } - val nameHighlight = normalizedName.contains(other = filter, ignoreCase = true) - if (nameHighlight.not() && playerTagHighlighted.not() && npcTagHighlighted.not()) { + if (nameHighlight.not()) { return null } } - - val tags = buildList { - if (characterId != null) { + // Tag filter process : Player. + if (tags[GMTagUio.TagId.PLAYER] == true && characterInstanceId == null) { + return null + } + // Tag filter process : Npc. + if (tags[GMTagUio.TagId.NPC] == true && npcIds.isEmpty()) { + return null + } + // Build the call tag list. + val previewTagsList = buildList { + if (characterInstanceId != null) { add( - GMCharacterPreviewUio.Tag( + GMTagUio( + id = GMTagUio.TagId.PLAYER, label = getString( Res.string.game_master__character_tag__character_label, - characterId.instanceId, + characterInstanceId.instanceId, ), - highlight = playerTagHighlighted, + highlight = tags[GMTagUio.TagId.PLAYER] ?: false, ) ) } addAll( npcIds.map { npcId -> - GMCharacterPreviewUio.Tag( + GMTagUio( + id = GMTagUio.TagId.NPC, label = getString( Res.string.game_master__character_tag__npc_label, npcId.instanceId ), - highlight = npcTagHighlighted, + highlight = tags[GMTagUio.TagId.NPC] ?: false, ) } ) } - + // build the cell action list val actions = buildList { add( - when (characterId) { + when (characterInstanceId) { null -> Action.AddToGroup - else -> Action.RemoveFromGroup(instanceId = characterId.instanceId) + else -> Action.RemoveFromGroup(instanceId = characterInstanceId.instanceId) } ) add(Action.AddToNpc) @@ -108,11 +105,11 @@ class GameMasterFactory { } ) } - + // return the cell UIO. return GMCharacterPreviewUio( characterSheetId = character.characterSheetId, name = character.name, level = character.level, - tags = tags, + tags = previewTagsList, actions = actions, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt index 0698ad4..abb8a77 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt @@ -3,6 +3,11 @@ 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.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.Column import androidx.compose.foundation.layout.PaddingValues @@ -10,7 +15,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +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.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -21,13 +29,18 @@ import androidx.compose.material.TopAppBar 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.Modifier import androidx.compose.ui.unit.dp 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.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.ic_cancel_24dp import org.jetbrains.compose.resources.painterResource @@ -38,6 +51,7 @@ fun GameMasterScreen( viewModel: GameMasterViewModel = koinViewModel(), ) { val characters = viewModel.characters.collectAsState() + val tags = viewModel.tags.collectAsState() Surface( modifier = Modifier.fillMaxSize() @@ -45,7 +59,9 @@ fun GameMasterScreen( GameMasterContent( modifier = Modifier.fillMaxSize(), filter = viewModel.filter, + tags = tags, characters = characters, + onTag = viewModel::onTag, onCharacterAction = viewModel::onCharacterAction, ) } @@ -54,10 +70,15 @@ fun GameMasterScreen( @Composable private fun GameMasterContent( modifier: Modifier = Modifier, + filterChipsState: LazyListState = rememberLazyListState(), filter: LwaTextFieldUio, + tags: State>, characters: State>, + onTag: (GMTagUio.TagId) -> Unit, onCharacterAction: (String, GMCharacterPreviewUio.Action) -> Unit, ) { + val scope = rememberCoroutineScope() + Scaffold( modifier = modifier, topBar = { @@ -95,9 +116,32 @@ private fun GameMasterContent( } } ) + 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 = PaddingValues(all = 8.dp), + contentPadding = remember { PaddingValues(all = 8.dp) }, verticalArrangement = Arrangement.spacedBy(space = 8.dp), ) { items( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterViewModel.kt index ddf37ad..dffbde7 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterViewModel.kt @@ -6,11 +6,18 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository 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.GMTagUio +import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio.TagId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +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__character_tag__character_search +import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_search +import org.jetbrains.compose.resources.getString class GameMasterViewModel( private val campaignRepository: CampaignRepository, @@ -28,10 +35,34 @@ class GameMasterViewModel( onValueChange = { _filter.value = it }, ) + private val _tags = MutableStateFlow(mapOf(TagId.PLAYER to false, TagId.NPC to false)) + val tags = _tags.map { it: Map -> + it.map { (tag, highlight) -> + when (tag) { + TagId.PLAYER -> GMTagUio( + id = TagId.PLAYER, + label = getString(Res.string.game_master__character_tag__character_search), + highlight = highlight, + ) + + TagId.NPC -> GMTagUio( + id = TagId.NPC, + label = getString(Res.string.game_master__character_tag__npc_search), + highlight = highlight, + ) + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList(), + ) + val characters = combine( campaignRepository.campaignFlow, characterSheetRepository.characterSheetPreviewFlow, filter.valueFlow, + _tags, gameMasterFactory::convertToGMCharacterPreviewUio, ).stateIn( scope = viewModelScope, @@ -54,4 +85,12 @@ class GameMasterViewModel( } } } + + fun onTag( + id: TagId, + ) { + _tags.value = _tags.value.toMutableMap().also { + it[id] = it.getOrPut(id) { true }.not() + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacterPreview.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacterPreview.kt index 34a8945..bf0261d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacterPreview.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacterPreview.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -44,15 +43,9 @@ data class GMCharacterPreviewUio( val characterSheetId: String, val name: String, val level: Int, - val tags: List, + val tags: List, val actions: List, ) { - @Stable - data class Tag( - val label: String, - val highlight: Boolean, - ) - @Stable sealed class Action { @Stable @@ -127,7 +120,10 @@ fun GMCharacterPreview( horizontalArrangement = Arrangement.spacedBy(space = 4.dp), ) { character.tags.forEach { tag -> - Tag(tag = tag) + GMTag( + style = MaterialTheme.lwa.typography.base.caption, + tag = tag, + ) } } } @@ -200,27 +196,4 @@ private fun OverflowActionMenu( } }, ) -} - - -@Composable -private fun Tag( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 2.dp), - tag: GMCharacterPreviewUio.Tag, -) { - Text( - modifier = modifier - .background( - color = MaterialTheme.lwa.colorScheme.elevated.base4dp, - shape = CircleShape, - ) - .padding(paddingValues = padding), - style = MaterialTheme.lwa.typography.base.caption, - color = when (tag.highlight) { - true -> MaterialTheme.lwa.colorScheme.base.secondary - else -> MaterialTheme.lwa.colorScheme.base.onSurface - }, - text = tag.label, - ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTagUio.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTagUio.kt new file mode 100644 index 0000000..eeca5b1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTagUio.kt @@ -0,0 +1,68 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.items + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa + + +@Stable +data class GMTagUio( + val id: TagId, + val label: String, + val highlight: Boolean, +) { + @Stable + enum class TagId { + PLAYER, NPC + } +} + +@Stable +object GmTagDefault { + val padding = PaddingValues(horizontal = 8.dp, vertical = 2.dp) +} + +@Composable +fun GMTag( + modifier: Modifier = Modifier, + padding: PaddingValues = GmTagDefault.padding, + shape: Shape = CircleShape, + elevation: Dp = 2.dp, + style: TextStyle, + tag: GMTagUio, + onTag: (() -> Unit)? = null, +) { + val animatedColor = animateColorAsState( + when (tag.highlight) { + true -> MaterialTheme.lwa.colorScheme.base.secondary + else -> MaterialTheme.lwa.colorScheme.base.onSurface + } + ) + Surface( + modifier = modifier, + shape = shape, + elevation = elevation, + ) { + Text( + modifier = Modifier + .clickable(enabled = onTag != null) { onTag?.invoke() } + .padding(paddingValues = padding), + style = style, + color = animatedColor.value, + text = tag.label, + ) + } +} \ No newline at end of file