GameMaster ui change to allow multiple screens

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2025-03-27 15:50:24 +01:00
parent 9d6f8e178b
commit 2fb0d3d4cd
29 changed files with 1034 additions and 513 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="M480,80q50,0 85,35t35,85q0,50 -35,85t-85,35q-50,0 -85,-35t-35,-85q0,-50 35,-85t85,-35ZM480,360q47,0 93,11t83,31q38,19 61,45t23,57v232q0,17 -8,33.5T710,800q-14,14 -32.5,26T636,848v-90q0,-38 -52.5,-62T480,672q-50,0 -96.5,20.5T326,746q38,15 78,21t82,7h34v104q-7,2 -14.5,2L490,880q-36,0 -82.5,-8T319,847q-42,-17 -70.5,-44.5T220,736v-232q0,-31 23,-57t60,-45q38,-20 84,-31t93,-11ZM480,600q33,0 56.5,-23.5T560,520q0,-33 -23.5,-56.5T480,440q-33,0 -56.5,23.5T400,520q0,33 23.5,56.5T480,600Z"
android:fillColor="#e3e3e3"/>
</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="M120,580q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6ZM120,420q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6ZM240,760q-17,0 -28.5,-11.5T200,720q0,-17 11.5,-28.5T240,680q17,0 28.5,11.5T280,720q0,17 -11.5,28.5T240,760ZM240,600q-17,0 -28.5,-11.5T200,560q0,-17 11.5,-28.5T240,520q17,0 28.5,11.5T280,560q0,17 -11.5,28.5T240,600ZM240,440q-17,0 -28.5,-11.5T200,400q0,-17 11.5,-28.5T240,360q17,0 28.5,11.5T280,400q0,17 -11.5,28.5T240,440ZM240,280q-17,0 -28.5,-11.5T200,240q0,-17 11.5,-28.5T240,200q17,0 28.5,11.5T280,240q0,17 -11.5,28.5T240,280ZM400,620q-25,0 -42.5,-17.5T340,560q0,-25 17.5,-42.5T400,500q25,0 42.5,17.5T460,560q0,25 -17.5,42.5T400,620ZM400,460q-25,0 -42.5,-17.5T340,400q0,-25 17.5,-42.5T400,340q25,0 42.5,17.5T460,400q0,25 -17.5,42.5T400,460ZM400,760q-17,0 -28.5,-11.5T360,720q0,-17 11.5,-28.5T400,680q17,0 28.5,11.5T440,720q0,17 -11.5,28.5T400,760ZM400,280q-17,0 -28.5,-11.5T360,240q0,-17 11.5,-28.5T400,200q17,0 28.5,11.5T440,240q0,17 -11.5,28.5T400,280ZM400,860q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6ZM400,140q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6ZM560,620q-25,0 -42.5,-17.5T500,560q0,-25 17.5,-42.5T560,500q25,0 42.5,17.5T620,560q0,25 -17.5,42.5T560,620ZM560,460q-25,0 -42.5,-17.5T500,400q0,-25 17.5,-42.5T560,340q25,0 42.5,17.5T620,400q0,25 -17.5,42.5T560,460ZM560,760q-17,0 -28.5,-11.5T520,720q0,-17 11.5,-28.5T560,680q17,0 28.5,11.5T600,720q0,17 -11.5,28.5T560,760ZM560,280q-17,0 -28.5,-11.5T520,240q0,-17 11.5,-28.5T560,200q17,0 28.5,11.5T600,240q0,17 -11.5,28.5T560,280ZM560,860q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6ZM560,140q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6ZM720,760q-17,0 -28.5,-11.5T680,720q0,-17 11.5,-28.5T720,680q17,0 28.5,11.5T760,720q0,17 -11.5,28.5T720,760ZM720,600q-17,0 -28.5,-11.5T680,560q0,-17 11.5,-28.5T720,520q17,0 28.5,11.5T760,560q0,17 -11.5,28.5T720,600ZM720,440q-17,0 -28.5,-11.5T680,400q0,-17 11.5,-28.5T720,360q17,0 28.5,11.5T760,400q0,17 -11.5,28.5T720,440ZM720,280q-17,0 -28.5,-11.5T680,240q0,-17 11.5,-28.5T720,200q17,0 28.5,11.5T760,240q0,17 -11.5,28.5T720,280ZM840,580q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6ZM840,420q-8,0 -14,-6t-6,-14q0,-8 6,-14t14,-6q8,0 14,6t6,14q0,8 -6,14t-14,6Z"
android:fillColor="#e3e3e3"/>
</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="M80,720v-120q0,-66 47,-113t113,-47h360v-40q0,-17 -11.5,-28.5T560,360L400,360q-17,0 -28.5,11.5T360,400h-80q0,-50 35,-85t85,-35h160q50,0 85,35t35,85v160q17,0 28.5,-11.5T720,520v-160q0,-50 35,-85t85,-35h40v80h-40q-17,0 -28.5,11.5T800,360v160q0,50 -35,85t-85,35v80L80,720ZM160,640h440v-120L240,520q-33,0 -56.5,23.5T160,600v40ZM600,640v-120,120Z"
android:fillColor="#e3e3e3"/>
</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="M120,840v-120h200q-84,-45 -132,-125t-48,-175q0,-142 99,-241t241,-99q142,0 241,99t99,241q0,95 -48,175T640,720h200v120L520,840v-204q78,-14 129,-75t51,-141q0,-92 -64,-156t-156,-64q-92,0 -156,64t-64,156q0,80 51,141t129,75v204L120,840Z"
android:fillColor="#e3e3e3"/>
</vector>

View file

@ -241,5 +241,6 @@
<string name="game_master__character_action__remove_from_group">Retirer du groupe</string>
<string name="game_master__character_action__add_to_npc">Ajouter aux Npcs</string>
<string name="game_master__character_action__remove_from_npc">Retirer des Npcs</string>
<string name="game_master__create_character_sheet">Créer un personnage</string>
</resources>

View file

@ -1,9 +1,6 @@
package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
@ -13,7 +10,6 @@ import androidx.compose.material.SnackbarDefaults
import androidx.compose.material.SnackbarDuration
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
@ -34,9 +30,8 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
import com.pixelized.desktop.lwa.ui.composable.LwaScaffold
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController
import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler
import com.pixelized.desktop.lwa.ui.composable.key.LocalKeyEventHandlers
import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost
@ -51,7 +46,6 @@ import com.pixelized.desktop.lwa.ui.navigation.window.destination.GameMasterWind
import com.pixelized.desktop.lwa.ui.navigation.window.destination.RollHistoryWindow
import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight
import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState
import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay
import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
@ -97,12 +91,8 @@ val LocalBlurController = compositionLocalOf<BlurContentController> {
@Preview
fun ApplicationScope.LwaApplication() {
val maxWindowHeight = rememberMaxWindowHeight()
val snackHostState = remember { SnackbarHostState() }
val errorSnackHostState = remember { SnackbarHostState() }
val windowController = remember { WindowController(maxWindowHeight) }
val keyEventHandlers = remember { mutableStateListOf<KeyEventHandler>() }
val rollHostState = remember { RollHostState() }
val blurController = rememberBlurContentController()
val windowsState = rememberWindowState(
size = DpSize(
width = 800.dp,
@ -121,14 +111,11 @@ fun ApplicationScope.LwaApplication() {
}
)
LwaTheme {
CompositionLocalProvider(
LocalApplicationScope provides this,
LocalSnackHost provides snackHostState,
LocalErrorSnackHost provides errorSnackHostState,
LocalWindowController provides windowController,
LocalKeyEventHandlers provides keyEventHandlers,
LocalRollHostState provides rollHostState,
LocalBlurController provides blurController,
LocalWindowState provides windowsState,
) {
Window(
@ -141,9 +128,13 @@ fun ApplicationScope.LwaApplication() {
) {
MainWindowScreen()
}
}
}
WindowsHandler(
windowController = windowController,
)
}
}
}
@Composable
private fun MainWindowScreen(
@ -154,66 +145,14 @@ private fun MainWindowScreen(
dataSyncViewModel.synchronise()
}
val snackHostState = LocalSnackHost.current
val errorSnackHostState = LocalErrorSnackHost.current
val windowController = LocalWindowController.current
val rollHostState = LocalRollHostState.current
val blurController = LocalBlurController.current
LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
Scaffold(
snackbarHost = {
Column(
modifier = Modifier.padding(all = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp)
) {
SnackbarHost(
hostState = snackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
actionColor = MaterialTheme.colors.onSurface,
)
}
)
SnackbarHost(
hostState = errorSnackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError,
actionColor = MaterialTheme.colors.onError,
)
}
)
}
},
content = {
BlurContent(
LwaScaffold(
modifier = Modifier.fillMaxSize(),
controller = blurController,
) {
MainNavHost()
}
RollOverlay(
modifier = Modifier.fillMaxSize(),
hostState = rollHostState,
)
}
)
NetworkSnackHandler(
snack = snackHostState,
snack = LocalSnackHost.current,
)
WindowsHandler(
windowController = windowController,
)
}
}
}
@ -239,7 +178,9 @@ private fun WindowsHandler(
is RollHistoryWindow -> RollHistoryPage()
is GameMasterWindow -> GameMasterScreen()
is GameMasterWindow -> LwaScaffold {
GameMasterScreen()
}
}
}
)

View file

@ -16,6 +16,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsStore
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedViewModel
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlayViewModel
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkFactory
@ -23,7 +24,6 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedViewModel
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.text.CampaignChatViewModel
@ -34,9 +34,10 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetV
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterActionUseCase
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.GMCharacterFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.GMCharacterViewModel
import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpFactory
import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
@ -120,7 +121,8 @@ val factoryDependencies
factoryOf(::CharacterSheetDiminishedDialogFactory)
factoryOf(::TextMessageFactory)
factoryOf(::LevelUpFactory)
factoryOf(::GameMasterFactory)
factoryOf(::GMCharacterFactory)
factoryOf(::GMActionViewModel)
}
val viewModelDependencies
@ -140,12 +142,12 @@ val viewModelDependencies
viewModelOf(::CampaignChatViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::LevelUpViewModel)
viewModelOf(::GameMasterViewModel)
viewModelOf(::PortraitOverlayViewModel)
viewModelOf(::GMCharacterViewModel)
viewModelOf(::GameMasterViewModel)
}
val useCaseDependencies
get() = module {
factoryOf(::SettingsUseCase)
factoryOf(::GameMasterActionUseCase)
}

View file

@ -0,0 +1,91 @@
package com.pixelized.desktop.lwa.ui.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.LocalErrorSnackHost
import com.pixelized.desktop.lwa.LocalRollHostState
import com.pixelized.desktop.lwa.LocalSnackHost
import com.pixelized.desktop.lwa.Snackbar
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController
import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState
import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay
@Composable
fun LwaScaffold(
modifier: Modifier = Modifier,
snackHostState: SnackbarHostState = remember { SnackbarHostState() },
errorSnackHostState: SnackbarHostState = remember { SnackbarHostState() },
rollHostState: RollHostState = remember { RollHostState() },
blurController: BlurContentController = rememberBlurContentController(),
content: @Composable BoxScope.() -> Unit,
) {
CompositionLocalProvider(
LocalSnackHost provides snackHostState,
LocalErrorSnackHost provides errorSnackHostState,
LocalRollHostState provides rollHostState,
LocalBlurController provides blurController,
) {
Surface(
modifier = modifier,
) {
Scaffold(
snackbarHost = {
Column(
modifier = Modifier.padding(all = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp)
) {
SnackbarHost(
hostState = snackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
actionColor = MaterialTheme.colors.onSurface,
)
}
)
SnackbarHost(
hostState = errorSnackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError,
actionColor = MaterialTheme.colors.onError,
)
}
)
}
},
content = {
BlurContent(
modifier = Modifier.fillMaxSize(),
controller = blurController,
content = content,
)
RollOverlay(
modifier = Modifier.fillMaxSize(),
hostState = rollHostState,
)
}
)
}
}
}

View file

@ -0,0 +1,30 @@
package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster
import androidx.compose.runtime.Stable
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionPage
@Stable
object GMActionDestination {
private const val ROUTE = "GameMasterAction"
fun baseRoute() = ROUTE
@Stable
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableGameMasterActionPage() {
composable(
route = GMActionDestination.baseRoute(),
) {
GMActionPage()
}
}
fun NavHostController.navigateToGameMasterActionPage() {
val route = GMActionDestination.navigationRoute()
navigate(route = route)
}

View file

@ -0,0 +1,25 @@
package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
object GMAlterationDestination {
private const val ROUTE = "GameMasterAlteration"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableGameMasterAlterationPage() {
composable(
route = GMAlterationDestination.baseRoute(),
) {
}
}
fun NavHostController.navigateToGameMasterAlterationPage() {
val route = GMAlterationDestination.navigationRoute()
navigate(route = route)
}

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.GMCharacterPage
object GMCharacterDestination {
private const val ROUTE = "GameMasterCharacter"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableGameMasterCharacterPage() {
composable(
route = GMCharacterDestination.baseRoute(),
) {
GMCharacterPage()
}
}
fun NavHostController.navigateToGameMasterCharacterPage() {
val route = GMCharacterDestination.navigationRoute()
navigate(route = route)
}

View file

@ -0,0 +1,25 @@
package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
object GMObjectDestination {
private const val ROUTE = "GameMasterObject"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableGameMasterObjectPage() {
composable(
route = GMObjectDestination.baseRoute(),
) {
}
}
fun NavHostController.navigateToGameMasterObjectPage() {
val route = GMObjectDestination.navigationRoute()
navigate(route = route)
}

View file

@ -21,7 +21,7 @@ fun WindowController.navigateToGameMasterWindow(
window = GameMasterWindow(
title = title,
size = DpSize(
width = 400.dp + 64.dp,
width = 124.dp * 4 + 64.dp,
height = maxWindowHeight - 32.dp,
)
)

View file

@ -29,7 +29,7 @@ class PlayerRibbonViewModel(
campaign: Campaign,
settings: Settings,
): Set<String> {
return if (campaign.options.showParty) campaign.characters else emptySet()
return if (campaign.options.showParty || settings.isGameMaster == true) campaign.characters else emptySet()
}
override fun hideOverruled(campaign: Campaign, settings: Settings): Boolean {

View file

@ -1,41 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio.Action
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
class GameMasterActionUseCase(
private val campaignRepository: CampaignRepository,
private val networkRepository: NetworkRepository,
) {
suspend fun handleAction(
characterSheetId: String,
action: Action,
) {
when (action) {
Action.DisplayPortrait -> networkRepository.share(
GameMasterEvent.DisplayPortrait(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
)
)
Action.AddToGroup -> campaignRepository.addCharacter(
characterSheetId = characterSheetId,
)
Action.AddToNpc -> campaignRepository.addNpc(
characterSheetId = characterSheetId,
)
is Action.RemoveFromGroup -> campaignRepository.removeCharacter(
characterSheetId = characterSheetId,
)
is Action.RemoveFromNpc -> campaignRepository.removeNpc(
characterSheetId = characterSheetId,
)
}
}
}

View file

@ -1,144 +1,93 @@
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.background
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.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.foundation.layout.width
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.Switch
import androidx.compose.material.Text
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.CompositionLocalProvider
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.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
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.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 androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMActionDestination
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterActionPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterAlterationPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterCharacterPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterObjectPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterActionPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterAlterationPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterCharacterPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterObjectPage
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTab
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTabUio
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaSwitchColors
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.game_master__action
import lwacharactersheet.composeapp.generated.resources.game_master__title
import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun GameMasterScreen(
viewModel: GameMasterViewModel = koinViewModel(),
) {
val windows = LocalWindowController.current
val scope = rememberCoroutineScope()
val characters = viewModel.characters.collectAsState()
val gameMaster = viewModel.gameMaster.collectAsState()
val npcVisibility = viewModel.npcVisibility.collectAsState()
val tags = viewModel.tags.collectAsState()
val screen = rememberNavController()
val gameMaster = viewModel.isGameMaster.collectAsState()
Surface(
modifier = Modifier.fillMaxSize()
) {
CompositionLocalProvider(
LocalScreenController provides screen,
) {
GameMasterContent(
modifier = Modifier.fillMaxSize(),
filter = viewModel.filter,
tags = tags,
controller = screen,
gameMaster = gameMaster,
npcVisibility = npcVisibility,
characters = characters,
onTag = viewModel::onTag,
onGameMaster = viewModel::onGameMaster,
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),
)
}
},
onNpcVisibility = {
scope.launch {
viewModel.onNpcVisibility()
onTab = {
when (it) {
GMTabUio.Actions -> screen.navigateToGameMasterActionPage()
GMTabUio.Characters -> screen.navigateToGameMasterCharacterPage()
GMTabUio.Alterations -> screen.navigateToGameMasterAlterationPage()
GMTabUio.Objects -> screen.navigateToGameMasterObjectPage()
}
},
)
}
}
}
@Composable
private fun GameMasterContent(
modifier: Modifier = Modifier,
padding: Dp = 16.dp,
spacing: Dp = 8.dp,
filterChipsState: LazyListState = rememberLazyListState(),
filter: LwaTextFieldUio,
tags: State<List<GMTagUio>>,
controller: NavHostController = rememberNavController(),
gameMaster: State<Boolean>,
npcVisibility: State<Boolean>,
characters: State<List<GMCharacterUio>>,
onGameMaster: (Boolean) -> Unit,
onTag: (GMTagUio.TagId) -> Unit,
onCharacterAction: (String, GMCharacterUio.Action) -> Unit,
onCharacterSheetEdit: (String) -> Unit,
onCharacterSheetCreate: () -> Unit,
onNpcVisibility: () -> Unit,
onTab: (GMTabUio) -> Unit,
) {
val scope = rememberCoroutineScope()
GameMasterLayout(
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
@ -171,160 +120,46 @@ private fun GameMasterContent(
}
)
},
content = {
Column {
content = { paddingValues ->
Row(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
) {
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(padding, spacing) {
PaddingValues(horizontal = padding, vertical = spacing)
},
horizontalArrangement = Arrangement.spacedBy(space = spacing),
) {
items(
items = tags.value,
) { tag ->
GMTag(
tag = tag,
onTag = { onTag(tag.id) },
)
}
}
}
}
Box(
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
.fillMaxHeight()
.width(width = 64.dp)
.padding(vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
LazyColumn(
modifier = Modifier.matchParentSize(),
contentPadding = remember {
PaddingValues(
start = padding,
top = padding,
end = padding,
bottom = padding + 48.dp + padding,
GMTabUio.entries.forEach {
GMTab(
tab = it,
onClick = { onTab(it) },
)
},
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
items(
items = characters.value,
key = { it.characterSheetId },
) { character ->
GMCharacter(
}
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.animateItem(),
character = character,
onEdit = {
onCharacterSheetEdit(character.characterSheetId)
},
onAction = { action ->
onCharacterAction(character.characterSheetId, action)
},
)
}
}
}
}
},
fab = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(all = padding),
horizontalArrangement = Arrangement.SpaceBetween,
.fillMaxHeight()
.weight(weight = 1f),
) {
IconButton(
modifier = Modifier.background(
color = MaterialTheme.lwa.colorScheme.base.primary,
shape = CircleShape,
),
onClick = onNpcVisibility,
NavHost(
modifier = Modifier.fillMaxSize(),
navController = controller,
startDestination = GMActionDestination.navigationRoute(),
) {
Icon(
painter = when (npcVisibility.value) {
true -> painterResource(Res.drawable.ic_visibility_off_24dp)
else -> painterResource(Res.drawable.ic_visibility_24dp)
},
tint = MaterialTheme.lwa.colorScheme.base.onPrimary,
contentDescription = null,
)
composableGameMasterActionPage()
composableGameMasterCharacterPage()
composableGameMasterAlterationPage()
composableGameMasterObjectPage()
}
IconButton(
modifier = Modifier.background(
color = MaterialTheme.lwa.colorScheme.base.primary,
shape = CircleShape,
),
onClick = onCharacterSheetCreate,
) {
Icon(
imageVector = Icons.Default.Add,
tint = MaterialTheme.lwa.colorScheme.base.onPrimary,
contentDescription = null,
)
}
}
}
)
}
@Composable
private fun GameMasterLayout(
modifier: Modifier,
topBar: @Composable () -> Unit,
content: @Composable () -> Unit,
fab: @Composable () -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = topBar,
content = { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = paddingValues)
) {
content()
Row(
modifier = Modifier.align(alignment = Alignment.BottomStart),
) {
fab()
}
}
}

View file

@ -2,83 +2,16 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster
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.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
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 com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
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 kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__character__filter
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc
import org.jetbrains.compose.resources.getString
class GameMasterViewModel(
campaignRepository: CampaignRepository,
characterSheetRepository: CharacterSheetRepository,
private val settingsRepository: SettingsRepository,
private val networkRepository: NetworkRepository,
private val factory: GameMasterFactory,
private val useCase: GameMasterActionUseCase,
): ViewModel() {
private val _filter = MutableStateFlow("")
val filter = LwaTextFieldUio(
enable = true,
labelFlow = MutableStateFlow(runBlocking { getString(Res.string.game_master__character__filter) }),
valueFlow = _filter,
isError = MutableStateFlow(false),
placeHolderFlow = MutableStateFlow(null),
onValueChange = { _filter.value = it },
)
private val _tags = MutableStateFlow(mapOf(TagId.PLAYER to false, TagId.NPC to false))
val tags = _tags.map { it: Map<TagId, Boolean> ->
it.map { (tag, highlight) ->
when (tag) {
TagId.PLAYER -> GMTagUio(
id = TagId.PLAYER,
label = getString(Res.string.game_master__character_tag__character),
highlight = highlight,
)
TagId.NPC -> GMTagUio(
id = TagId.NPC,
label = getString(Res.string.game_master__character_tag__npc),
highlight = highlight,
)
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
val characters = combine(
campaignRepository.campaignFlow,
characterSheetRepository.characterSheetPreviewFlow,
filter.valueFlow,
_tags,
factory::convertToGMCharacterPreviewUio,
).stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
val gameMaster = settingsRepository.settingsFlow()
val isGameMaster = settingsRepository.settingsFlow()
.map { it.isGameMaster ?: false }
.stateIn(
scope = viewModelScope,
@ -86,14 +19,6 @@ class GameMasterViewModel(
initialValue = false,
)
val npcVisibility = campaignRepository.campaignFlow
.map { it.options.showNpcs }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
fun onGameMaster(value: Boolean) {
val settings = settingsRepository.settings()
settingsRepository.update(
@ -102,36 +27,4 @@ class GameMasterViewModel(
)
)
}
fun onCharacterAction(
characterSheetId: String,
action: GMCharacterUio.Action,
) {
viewModelScope.launch {
try {
useCase.handleAction(
characterSheetId = characterSheetId,
action = action,
)
} catch (exception: Exception) {
// TODO
}
}
}
fun onTag(
id: TagId,
) {
_tags.value = _tags.value.toMutableMap().also {
it[id] = it.getOrPut(id) { true }.not()
}
}
suspend fun onNpcVisibility() {
networkRepository.share(
GameMasterEvent.ToggleNpc(
timestamp = System.currentTimeMillis(),
)
)
}
}

View file

@ -0,0 +1,99 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.action
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.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 com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAction
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp
import org.koin.compose.viewmodel.koinViewModel
@Stable
data class ActionPageUio(
val party: Boolean,
val npc: Boolean,
)
@Composable
fun GMActionPage(
viewModel: GMActionViewModel = koinViewModel(),
) {
val scope = rememberCoroutineScope()
val scroll = rememberScrollState()
val actions = viewModel.actions.collectAsState()
GMActionContent(
actions = actions,
scroll = scroll,
onPartyVisibility = {
scope.launch {
viewModel.onPlayerVisibility()
}
},
onNpcVisibility = {
scope.launch {
viewModel.onNpcVisibility()
}
},
)
}
@Composable
fun GMActionContent(
modifier: Modifier = Modifier,
scroll: ScrollState,
spacing: Dp = 8.dp,
actions: State<ActionPageUio?>,
onPartyVisibility: () -> Unit,
onNpcVisibility: () -> Unit,
) {
Column(
modifier = modifier
.verticalScroll(state = scroll)
.padding(vertical = spacing, horizontal = spacing),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
actions.value?.party?.let { party ->
GMAction(
modifier = Modifier.fillMaxWidth(),
icon = when (party) {
true -> Res.drawable.ic_visibility_off_24dp
else -> Res.drawable.ic_visibility_24dp
},
label = when (party) {
true -> "Cacher les Joueurs"
else -> "Afficher les Joueurs"
},
onAction = onPartyVisibility,
)
}
actions.value?.npc?.let { npc ->
GMAction(
modifier = Modifier.fillMaxWidth(),
icon = when (npc) {
true -> Res.drawable.ic_visibility_off_24dp
else -> Res.drawable.ic_visibility_24dp
},
label = when (npc) {
true -> "Cacher les NPCs"
else -> "Afficher les NPCs"
},
onAction = onNpcVisibility,
)
}
}
}

View file

@ -0,0 +1,48 @@
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.network.NetworkRepository
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
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
class GMActionViewModel(
private val networkRepository: NetworkRepository,
campaignRepository: CampaignRepository,
) : ViewModel() {
val actions: StateFlow<ActionPageUio?> = campaignRepository.campaignFlow
.map {
ActionPageUio(
party = it.options.showParty,
npc = it.options.showNpcs,
)
}
.distinctUntilChanged()
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = null,
)
suspend fun onNpcVisibility() {
networkRepository.share(
GameMasterEvent.ToggleNpc(
timestamp = System.currentTimeMillis(),
)
)
}
suspend fun onPlayerVisibility() {
networkRepository.share(
GameMasterEvent.TogglePlayer(
timestamp = System.currentTimeMillis(),
)
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster
package com.pixelized.desktop.lwa.ui.screen.gamemaster.character
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.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio
import com.pixelized.desktop.lwa.utils.extention.unAccent
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview
@ -11,14 +11,14 @@ import lwacharactersheet.composeapp.generated.resources.game_master__character_t
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc
import org.jetbrains.compose.resources.getString
class GameMasterFactory {
class GMCharacterFactory {
suspend fun convertToGMCharacterPreviewUio(
campaign: Campaign,
characters: List<CharacterSheetPreview>,
filter: String,
tags: Map<GMTagUio.TagId, Boolean>,
): List<GMCharacterUio> {
tags: Map<GMTagItemUio.TagId, Boolean>,
): List<GMCharacterItemUio> {
val normalizedFilter = filter.unAccent()
return characters.mapNotNull {
@ -35,8 +35,8 @@ class GameMasterFactory {
campaign: Campaign,
character: CharacterSheetPreview,
filter: String,
tags: Map<GMTagUio.TagId, Boolean>,
): GMCharacterUio? {
tags: Map<GMTagItemUio.TagId, Boolean>,
): GMCharacterItemUio? {
// get the characterInstanceId from the player list corresponding to this CharacterSheet if any
val isPlayer = campaign.characters.firstOrNull {
it == character.characterSheetId
@ -55,30 +55,30 @@ class GameMasterFactory {
}
}
// Tag filter process : Player.
if (tags[GMTagUio.TagId.PLAYER] == true && isPlayer.not()) {
if (tags[GMTagItemUio.TagId.PLAYER] == true && isPlayer.not()) {
return null
}
// Tag filter process : Npc.
if (tags[GMTagUio.TagId.NPC] == true && isNpc.not()) {
if (tags[GMTagItemUio.TagId.NPC] == true && isNpc.not()) {
return null
}
// Build the call tag list.
val previewTagsList = buildList {
if (isPlayer) {
add(
GMTagUio(
id = GMTagUio.TagId.PLAYER,
GMTagItemUio(
id = GMTagItemUio.TagId.PLAYER,
label = getString(Res.string.game_master__character_tag__character),
highlight = tags[GMTagUio.TagId.PLAYER] ?: false,
highlight = tags[GMTagItemUio.TagId.PLAYER] ?: false,
)
)
}
if (isNpc) {
add(
GMTagUio(
id = GMTagUio.TagId.NPC,
GMTagItemUio(
id = GMTagItemUio.TagId.NPC,
label = getString(Res.string.game_master__character_tag__npc),
highlight = tags[GMTagUio.TagId.NPC] ?: false,
highlight = tags[GMTagItemUio.TagId.NPC] ?: false,
)
)
}
@ -96,7 +96,7 @@ class GameMasterFactory {
}
}
// return the cell UIO.
return GMCharacterUio(
return GMCharacterItemUio(
characterSheetId = character.characterSheetId,
name = character.name,
level = character.level,

View file

@ -0,0 +1,165 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.character
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
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 androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
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.GMCharacterItemUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMFilterHeader
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
import kotlinx.coroutines.flow.StateFlow
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.game_master__create_character_sheet
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun GMCharacterPage(
viewModel: GMCharacterViewModel = koinViewModel(),
) {
val windows = LocalWindowController.current
val scope = rememberCoroutineScope()
val characters = viewModel.characters.collectAsState()
val tags = viewModel.tags.collectAsState()
GMCharacterContent(
filter = viewModel.filter,
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),
)
}
},
)
}
@Composable
fun GMCharacterContent(
modifier: Modifier = Modifier,
padding: Dp = 8.dp,
spacing: Dp = 8.dp,
filter: LwaTextFieldUio,
tags: State<List<GMTagItemUio>>,
characters: State<List<GMCharacterItemUio>>,
onTag: (GMTagItemUio.TagId) -> Unit,
onCharacterAction: (String, GMCharacterItemUio.Action) -> Unit,
onCharacterSheetEdit: (String) -> Unit,
onCharacterSheetCreate: () -> Unit,
) {
Column(
modifier = modifier,
) {
Surface(
elevation = 1.dp,
) {
GMFilterHeader(
padding = padding,
spacing = spacing,
filter = filter,
tags = tags,
onTag = onTag,
)
}
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
) {
LazyColumn(
modifier = Modifier.matchParentSize(),
contentPadding = remember {
PaddingValues(
start = padding,
top = padding,
end = padding,
bottom = padding + 48.dp + padding,
)
},
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
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)
},
onTag = onTag,
)
}
}
Row(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(all = padding),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Button(
colors = LwaButtonColors(),
shape = CircleShape,
onClick = onCharacterSheetCreate,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.game_master__create_character_sheet),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}

View file

@ -0,0 +1,122 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.character
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.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio.TagId
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
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 kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__character__filter
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc
import org.jetbrains.compose.resources.getString
class GMCharacterViewModel(
private val networkRepository: NetworkRepository,
private val campaignRepository: CampaignRepository,
characterSheetRepository: CharacterSheetRepository,
private val factory: GMCharacterFactory,
) : ViewModel() {
private val _filter = MutableStateFlow("")
val filter = LwaTextFieldUio(
enable = true,
labelFlow = MutableStateFlow(runBlocking { getString(Res.string.game_master__character__filter) }),
valueFlow = _filter,
isError = MutableStateFlow(false),
placeHolderFlow = MutableStateFlow(null),
onValueChange = { _filter.value = it },
)
private val _tags = MutableStateFlow(mapOf(TagId.PLAYER to false, TagId.NPC to false))
val tags = _tags.map { it: Map<TagId, Boolean> ->
it.map { (tag, highlight) ->
when (tag) {
TagId.PLAYER -> GMTagItemUio(
id = TagId.PLAYER,
label = getString(Res.string.game_master__character_tag__character),
highlight = highlight,
)
TagId.NPC -> GMTagItemUio(
id = TagId.NPC,
label = getString(Res.string.game_master__character_tag__npc),
highlight = highlight,
)
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
val characters = combine(
campaignRepository.campaignFlow,
characterSheetRepository.characterSheetPreviewFlow,
filter.valueFlow,
_tags,
factory::convertToGMCharacterPreviewUio,
).stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
fun onCharacterAction(
characterSheetId: String,
action: Action,
) {
viewModelScope.launch {
try {
when (action) {
Action.DisplayPortrait -> networkRepository.share(
GameMasterEvent.DisplayPortrait(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
)
)
Action.AddToGroup -> campaignRepository.addCharacter(
characterSheetId = characterSheetId,
)
Action.AddToNpc -> campaignRepository.addNpc(
characterSheetId = characterSheetId,
)
Action.RemoveFromGroup -> campaignRepository.removeCharacter(
characterSheetId = characterSheetId,
)
Action.RemoveFromNpc -> campaignRepository.removeNpc(
characterSheetId = characterSheetId,
)
}
} catch (exception: Exception) {
// TODO
}
}
}
fun onTag(
id: TagId,
) {
_tags.value = _tags.value.toMutableMap().also {
it[id] = it.getOrPut(id) { true }.not()
}
}
}

View file

@ -0,0 +1,60 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.items
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
@Stable
object GmActionDefault {
val padding = PaddingValues(horizontal = 16.dp)
}
@Composable
fun GMAction(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = GmActionDefault.padding,
icon: DrawableResource,
label: String,
onAction: () -> Unit,
) {
Row(
modifier = Modifier
.clip(shape = MaterialTheme.lwa.shapes.gameMaster)
.clickable(onClick = onAction)
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.minimumInteractiveComponentSize()
.padding(paddingValues = paddingValues)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(icon),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
style = MaterialTheme.lwa.typography.base.body1,
text = label,
)
}
}

View file

@ -27,7 +27,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.GMCharacterUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio.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
@ -46,11 +46,11 @@ import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable
data class GMCharacterUio(
data class GMCharacterItemUio(
val characterSheetId: String,
val name: String,
val level: Int,
val tags: List<GMTagUio>,
val tags: List<GMTagItemUio>,
val actions: List<Action>,
) {
@Stable
@ -92,16 +92,17 @@ object GMCharacterPreviewDefault {
fun GMCharacter(
modifier: Modifier = Modifier,
padding: PaddingValues = GMCharacterPreviewDefault.padding,
character: GMCharacterUio,
character: GMCharacterItemUio,
onEdit: () -> Unit,
onAction: (Action) -> Unit,
onTag: (GMTagItemUio.TagId) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val startPadding = padding.calculateStartPadding(layoutDirection)
Box(
modifier = Modifier
.clip(shape = remember { RoundedCornerShape(8.dp) })
.clip(shape = MaterialTheme.lwa.shapes.gameMaster)
.clickable(onClick = onEdit)
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.then(other = modifier),
@ -109,6 +110,7 @@ fun GMCharacter(
Row(
modifier = Modifier.padding(start = startPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Row(
modifier = Modifier.weight(weight = 1f),
@ -133,6 +135,7 @@ fun GMCharacter(
GMTag(
elevation = 4.dp,
tag = tag,
onTag = { onTag(tag.id) }
)
}
@ -147,7 +150,7 @@ fun GMCharacter(
@Composable
private fun OverflowActionMenu(
modifier: Modifier = Modifier,
character: GMCharacterUio,
character: GMCharacterItemUio,
onAction: (Action) -> Unit,
) {
val overflowMenu = remember(character) {

View file

@ -0,0 +1,100 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.items
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
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
import androidx.compose.foundation.layout.fillMaxWidth
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
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 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.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
@Composable
fun GMFilterHeader(
modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(),
padding: Dp = 16.dp,
spacing: Dp = 8.dp,
filter: LwaTextFieldUio,
tags: State<List<GMTagItemUio>>,
onTag: (GMTagItemUio.TagId) -> Unit,
) {
val scope = rememberCoroutineScope()
Column(
modifier = modifier,
) {
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 {
lazyListState.scrollBy(-delta)
}
},
),
state = lazyListState,
contentPadding = remember(padding, spacing) {
PaddingValues(horizontal = padding, vertical = spacing)
},
horizontalArrangement = Arrangement.spacedBy(space = spacing),
) {
items(
items = tags.value,
) { tag ->
GMTag(
tag = tag,
onTag = { onTag(tag.id) },
)
}
}
}
}

View file

@ -0,0 +1,41 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.items
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_account_child_invert_24dp
import lwacharactersheet.composeapp.generated.resources.ic_blur_on_24dp
import lwacharactersheet.composeapp.generated.resources.ic_iron_24dp
import lwacharactersheet.composeapp.generated.resources.ic_special_character_24dp
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
@Stable
enum class GMTabUio(
val icon: DrawableResource,
) {
Actions(icon = Res.drawable.ic_special_character_24dp),
Characters(icon = Res.drawable.ic_account_child_invert_24dp),
Alterations(icon = Res.drawable.ic_blur_on_24dp),
Objects(icon = Res.drawable.ic_iron_24dp),
}
@Composable
fun GMTab(
tab: GMTabUio,
onClick: () -> Unit,
) {
IconButton(
onClick = onClick
) {
Icon(
painter = painterResource(tab.icon),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}

View file

@ -12,14 +12,13 @@ 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(
data class GMTagItemUio(
val id: TagId,
val label: String,
val highlight: Boolean,
@ -41,7 +40,7 @@ fun GMTag(
padding: PaddingValues = GmTagDefault.padding,
shape: Shape = CircleShape,
elevation: Dp = 2.dp,
tag: GMTagUio,
tag: GMTagItemUio,
onTag: (() -> Unit)? = null,
) {
val animatedColor = animateColorAsState(

View file

@ -0,0 +1,17 @@
package com.pixelized.desktop.lwa.ui.theme.color.component
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@Composable
@Stable
fun LwaButtonColors(): ButtonColors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.primary,
disabledContentColor = MaterialTheme.colors.surface.copy(alpha = ContentAlpha.disabled),
)

View file

@ -12,6 +12,7 @@ data class LwaShapes(
val portrait: Shape,
val panel: Shape,
val settings: Shape,
val gameMaster: Shape,
)
@Stable
@ -20,10 +21,12 @@ fun lwaShapes(
portrait: Shape = RoundedCornerShape(8.dp),
panel: Shape = RoundedCornerShape(8.dp),
settings: Shape = RoundedCornerShape(8.dp),
gameMaster: Shape = RoundedCornerShape(8.dp),
): LwaShapes = remember {
LwaShapes(
portrait = portrait,
panel = panel,
settings = settings,
gameMaster = gameMaster,
)
}