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__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__add_to_npc">Ajouter aux Npcs</string>
<string name="game_master__character_action__remove_from_npc">Retirer des 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> </resources>

View file

@ -1,9 +1,6 @@
package com.pixelized.desktop.lwa 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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
@ -13,7 +10,6 @@ import androidx.compose.material.SnackbarDefaults
import androidx.compose.material.SnackbarDuration import androidx.compose.material.SnackbarDuration
import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -34,9 +30,8 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status 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.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.KeyEventHandler
import com.pixelized.desktop.lwa.ui.composable.key.LocalKeyEventHandlers import com.pixelized.desktop.lwa.ui.composable.key.LocalKeyEventHandlers
import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost 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.destination.RollHistoryWindow
import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight 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.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.characterSheet.CharacterSheetMainNavHost
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
@ -97,12 +91,8 @@ val LocalBlurController = compositionLocalOf<BlurContentController> {
@Preview @Preview
fun ApplicationScope.LwaApplication() { fun ApplicationScope.LwaApplication() {
val maxWindowHeight = rememberMaxWindowHeight() val maxWindowHeight = rememberMaxWindowHeight()
val snackHostState = remember { SnackbarHostState() }
val errorSnackHostState = remember { SnackbarHostState() }
val windowController = remember { WindowController(maxWindowHeight) } val windowController = remember { WindowController(maxWindowHeight) }
val keyEventHandlers = remember { mutableStateListOf<KeyEventHandler>() } val keyEventHandlers = remember { mutableStateListOf<KeyEventHandler>() }
val rollHostState = remember { RollHostState() }
val blurController = rememberBlurContentController()
val windowsState = rememberWindowState( val windowsState = rememberWindowState(
size = DpSize( size = DpSize(
width = 800.dp, width = 800.dp,
@ -121,30 +111,31 @@ fun ApplicationScope.LwaApplication() {
} }
) )
CompositionLocalProvider( LwaTheme {
LocalApplicationScope provides this, CompositionLocalProvider(
LocalSnackHost provides snackHostState, LocalApplicationScope provides this,
LocalErrorSnackHost provides errorSnackHostState, LocalWindowController provides windowController,
LocalWindowController provides windowController, LocalKeyEventHandlers provides keyEventHandlers,
LocalKeyEventHandlers provides keyEventHandlers, LocalWindowState provides windowsState,
LocalRollHostState provides rollHostState,
LocalBlurController provides blurController,
LocalWindowState provides windowsState,
) {
Window(
onCloseRequest = ::exitApplication,
state = windowsState,
title = runBlocking { getString(Res.string.app_name) },
onKeyEvent = { event ->
keyEventHandlers.reversed().any { it(event) }
},
) { ) {
MainWindowScreen() Window(
onCloseRequest = ::exitApplication,
state = windowsState,
title = runBlocking { getString(Res.string.app_name) },
onKeyEvent = { event ->
keyEventHandlers.reversed().any { it(event) }
},
) {
MainWindowScreen()
}
WindowsHandler(
windowController = windowController,
)
} }
} }
} }
@Composable @Composable
private fun MainWindowScreen( private fun MainWindowScreen(
dataSyncViewModel: DataSyncViewModel = koinViewModel(), dataSyncViewModel: DataSyncViewModel = koinViewModel(),
@ -154,66 +145,14 @@ private fun MainWindowScreen(
dataSyncViewModel.synchronise() dataSyncViewModel.synchronise()
} }
val snackHostState = LocalSnackHost.current LwaScaffold(
val errorSnackHostState = LocalErrorSnackHost.current modifier = Modifier.fillMaxSize(),
val windowController = LocalWindowController.current ) {
val rollHostState = LocalRollHostState.current MainNavHost()
val blurController = LocalBlurController.current
LwaTheme { NetworkSnackHandler(
Surface( snack = LocalSnackHost.current,
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(
modifier = Modifier.fillMaxSize(),
controller = blurController,
) {
MainNavHost()
}
RollOverlay(
modifier = Modifier.fillMaxSize(),
hostState = rollHostState,
)
}
)
NetworkSnackHandler(
snack = snackHostState,
)
WindowsHandler(
windowController = windowController,
)
}
} }
} }
@ -239,7 +178,9 @@ private fun WindowsHandler(
is RollHistoryWindow -> RollHistoryPage() 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.CharacterDetailCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory 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.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.portrait.PortraitOverlayViewModel
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkFactory 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.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory 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.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.npc.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel 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.CharacterSheetEditFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditViewModel 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.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.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.LevelUpFactory
import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
@ -120,7 +121,8 @@ val factoryDependencies
factoryOf(::CharacterSheetDiminishedDialogFactory) factoryOf(::CharacterSheetDiminishedDialogFactory)
factoryOf(::TextMessageFactory) factoryOf(::TextMessageFactory)
factoryOf(::LevelUpFactory) factoryOf(::LevelUpFactory)
factoryOf(::GameMasterFactory) factoryOf(::GMCharacterFactory)
factoryOf(::GMActionViewModel)
} }
val viewModelDependencies val viewModelDependencies
@ -140,12 +142,12 @@ val viewModelDependencies
viewModelOf(::CampaignChatViewModel) viewModelOf(::CampaignChatViewModel)
viewModelOf(::SettingsViewModel) viewModelOf(::SettingsViewModel)
viewModelOf(::LevelUpViewModel) viewModelOf(::LevelUpViewModel)
viewModelOf(::GameMasterViewModel)
viewModelOf(::PortraitOverlayViewModel) viewModelOf(::PortraitOverlayViewModel)
viewModelOf(::GMCharacterViewModel)
viewModelOf(::GameMasterViewModel)
} }
val useCaseDependencies val useCaseDependencies
get() = module { get() = module {
factoryOf(::SettingsUseCase) 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( window = GameMasterWindow(
title = title, title = title,
size = DpSize( size = DpSize(
width = 400.dp + 64.dp, width = 124.dp * 4 + 64.dp,
height = maxWindowHeight - 32.dp, height = maxWindowHeight - 32.dp,
) )
) )

View file

@ -29,7 +29,7 @@ class PlayerRibbonViewModel(
campaign: Campaign, campaign: Campaign,
settings: Settings, settings: Settings,
): Set<String> { ): 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 { 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 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.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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.layout.width
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.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Switch import androidx.compose.material.Switch
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TopAppBar 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.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController import androidx.navigation.NavHostController
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField import androidx.navigation.compose.NavHost
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacter import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMActionDestination
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterActionPage
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTag import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterAlterationPage
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio 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.color.component.LwaSwitchColors
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res 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__action
import lwacharactersheet.composeapp.generated.resources.game_master__title 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.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
fun GameMasterScreen( fun GameMasterScreen(
viewModel: GameMasterViewModel = koinViewModel(), viewModel: GameMasterViewModel = koinViewModel(),
) { ) {
val windows = LocalWindowController.current val screen = rememberNavController()
val scope = rememberCoroutineScope() val gameMaster = viewModel.isGameMaster.collectAsState()
val characters = viewModel.characters.collectAsState()
val gameMaster = viewModel.gameMaster.collectAsState()
val npcVisibility = viewModel.npcVisibility.collectAsState()
val tags = viewModel.tags.collectAsState()
Surface( Surface(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
GameMasterContent( CompositionLocalProvider(
modifier = Modifier.fillMaxSize(), LocalScreenController provides screen,
filter = viewModel.filter, ) {
tags = tags, GameMasterContent(
gameMaster = gameMaster, modifier = Modifier.fillMaxSize(),
npcVisibility = npcVisibility, controller = screen,
characters = characters, gameMaster = gameMaster,
onTag = viewModel::onTag, onGameMaster = viewModel::onGameMaster,
onGameMaster = viewModel::onGameMaster, onTab = {
onCharacterAction = viewModel::onCharacterAction, when (it) {
onCharacterSheetEdit = { characterSheetId -> GMTabUio.Actions -> screen.navigateToGameMasterActionPage()
scope.launch { GMTabUio.Characters -> screen.navigateToGameMasterCharacterPage()
windows.navigateToCharacterSheetEdit( GMTabUio.Alterations -> screen.navigateToGameMasterAlterationPage()
characterId = characterSheetId, GMTabUio.Objects -> screen.navigateToGameMasterObjectPage()
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()
}
},
)
} }
} }
@Composable @Composable
private fun GameMasterContent( private fun GameMasterContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
padding: Dp = 16.dp, controller: NavHostController = rememberNavController(),
spacing: Dp = 8.dp,
filterChipsState: LazyListState = rememberLazyListState(),
filter: LwaTextFieldUio,
tags: State<List<GMTagUio>>,
gameMaster: State<Boolean>, gameMaster: State<Boolean>,
npcVisibility: State<Boolean>,
characters: State<List<GMCharacterUio>>,
onGameMaster: (Boolean) -> Unit, onGameMaster: (Boolean) -> Unit,
onTag: (GMTagUio.TagId) -> Unit, onTab: (GMTabUio) -> Unit,
onCharacterAction: (String, GMCharacterUio.Action) -> Unit,
onCharacterSheetEdit: (String) -> Unit,
onCharacterSheetCreate: () -> Unit,
onNpcVisibility: () -> Unit,
) { ) {
val scope = rememberCoroutineScope() Scaffold(
GameMasterLayout(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -171,160 +120,46 @@ private fun GameMasterContent(
} }
) )
}, },
content = { content = { paddingValues ->
Column { Row(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
) {
Surface( Surface(
elevation = 1.dp, elevation = 1.dp,
) { ) {
Column { Column(
LwaTextField( modifier = Modifier
modifier = Modifier.fillMaxWidth(), .fillMaxHeight()
field = filter, .width(width = 64.dp)
trailingIcon = { .padding(vertical = 8.dp),
val value = filter.valueFlow.collectAsState() horizontalAlignment = Alignment.CenterHorizontally,
AnimatedVisibility( verticalArrangement = Arrangement.spacedBy(space = 8.dp),
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(
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( GMTabUio.entries.forEach {
items = characters.value, GMTab(
key = { it.characterSheetId }, tab = it,
) { character -> onClick = { onTab(it) },
GMCharacter(
modifier = Modifier
.fillMaxWidth()
.animateItem(),
character = character,
onEdit = {
onCharacterSheetEdit(character.characterSheetId)
},
onAction = { action ->
onCharacterAction(character.characterSheetId, action)
},
) )
} }
} }
} }
} Surface(
}, modifier = Modifier
fab = { .fillMaxHeight()
Row( .weight(weight = 1f),
modifier = Modifier
.fillMaxWidth()
.padding(all = padding),
horizontalArrangement = Arrangement.SpaceBetween,
) {
IconButton(
modifier = Modifier.background(
color = MaterialTheme.lwa.colorScheme.base.primary,
shape = CircleShape,
),
onClick = onNpcVisibility,
) { ) {
Icon( NavHost(
painter = when (npcVisibility.value) { modifier = Modifier.fillMaxSize(),
true -> painterResource(Res.drawable.ic_visibility_off_24dp) navController = controller,
else -> painterResource(Res.drawable.ic_visibility_24dp) startDestination = GMActionDestination.navigationRoute(),
}, ) {
tint = MaterialTheme.lwa.colorScheme.base.onPrimary, composableGameMasterActionPage()
contentDescription = null, 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.ViewModel
import androidx.lifecycle.viewModelScope 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.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.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn 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( class GameMasterViewModel(
campaignRepository: CampaignRepository,
characterSheetRepository: CharacterSheetRepository,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val networkRepository: NetworkRepository, ): ViewModel() {
private val factory: GameMasterFactory,
private val useCase: GameMasterActionUseCase,
) : ViewModel() {
private val _filter = MutableStateFlow("") val isGameMaster = settingsRepository.settingsFlow()
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()
.map { it.isGameMaster ?: false } .map { it.isGameMaster ?: false }
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
@ -86,14 +19,6 @@ class GameMasterViewModel(
initialValue = false, initialValue = false,
) )
val npcVisibility = campaignRepository.campaignFlow
.map { it.options.showNpcs }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
fun onGameMaster(value: Boolean) { fun onGameMaster(value: Boolean) {
val settings = settingsRepository.settings() val settings = settingsRepository.settings()
settingsRepository.update( 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.GMCharacterItemUio
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.screen.gamemaster.items.GMTagUio import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio
import com.pixelized.desktop.lwa.utils.extention.unAccent import com.pixelized.desktop.lwa.utils.extention.unAccent
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview 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 lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
class GameMasterFactory { class GMCharacterFactory {
suspend fun convertToGMCharacterPreviewUio( suspend fun convertToGMCharacterPreviewUio(
campaign: Campaign, campaign: Campaign,
characters: List<CharacterSheetPreview>, characters: List<CharacterSheetPreview>,
filter: String, filter: String,
tags: Map<GMTagUio.TagId, Boolean>, tags: Map<GMTagItemUio.TagId, Boolean>,
): List<GMCharacterUio> { ): List<GMCharacterItemUio> {
val normalizedFilter = filter.unAccent() val normalizedFilter = filter.unAccent()
return characters.mapNotNull { return characters.mapNotNull {
@ -35,8 +35,8 @@ class GameMasterFactory {
campaign: Campaign, campaign: Campaign,
character: CharacterSheetPreview, character: CharacterSheetPreview,
filter: String, filter: String,
tags: Map<GMTagUio.TagId, Boolean>, tags: Map<GMTagItemUio.TagId, Boolean>,
): GMCharacterUio? { ): GMCharacterItemUio? {
// get the characterInstanceId from the player list corresponding to this CharacterSheet if any // get the characterInstanceId from the player list corresponding to this CharacterSheet if any
val isPlayer = campaign.characters.firstOrNull { val isPlayer = campaign.characters.firstOrNull {
it == character.characterSheetId it == character.characterSheetId
@ -55,30 +55,30 @@ class GameMasterFactory {
} }
} }
// Tag filter process : Player. // Tag filter process : Player.
if (tags[GMTagUio.TagId.PLAYER] == true && isPlayer.not()) { if (tags[GMTagItemUio.TagId.PLAYER] == true && isPlayer.not()) {
return null return null
} }
// Tag filter process : Npc. // Tag filter process : Npc.
if (tags[GMTagUio.TagId.NPC] == true && isNpc.not()) { if (tags[GMTagItemUio.TagId.NPC] == true && isNpc.not()) {
return null return null
} }
// Build the call tag list. // Build the call tag list.
val previewTagsList = buildList { val previewTagsList = buildList {
if (isPlayer) { if (isPlayer) {
add( add(
GMTagUio( GMTagItemUio(
id = GMTagUio.TagId.PLAYER, id = GMTagItemUio.TagId.PLAYER,
label = getString(Res.string.game_master__character_tag__character), label = getString(Res.string.game_master__character_tag__character),
highlight = tags[GMTagUio.TagId.PLAYER] ?: false, highlight = tags[GMTagItemUio.TagId.PLAYER] ?: false,
) )
) )
} }
if (isNpc) { if (isNpc) {
add( add(
GMTagUio( GMTagItemUio(
id = GMTagUio.TagId.NPC, id = GMTagItemUio.TagId.NPC,
label = getString(Res.string.game_master__character_tag__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 the cell UIO.
return GMCharacterUio( return GMCharacterItemUio(
characterSheetId = character.characterSheetId, characterSheetId = character.characterSheetId,
name = character.name, name = character.name,
level = character.level, 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.platform.LocalLayoutDirection
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp 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 com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__add_to_group 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 import org.jetbrains.compose.resources.stringResource
@Stable @Stable
data class GMCharacterUio( data class GMCharacterItemUio(
val characterSheetId: String, val characterSheetId: String,
val name: String, val name: String,
val level: Int, val level: Int,
val tags: List<GMTagUio>, val tags: List<GMTagItemUio>,
val actions: List<Action>, val actions: List<Action>,
) { ) {
@Stable @Stable
@ -92,16 +92,17 @@ object GMCharacterPreviewDefault {
fun GMCharacter( fun GMCharacter(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
padding: PaddingValues = GMCharacterPreviewDefault.padding, padding: PaddingValues = GMCharacterPreviewDefault.padding,
character: GMCharacterUio, character: GMCharacterItemUio,
onEdit: () -> Unit, onEdit: () -> Unit,
onAction: (Action) -> Unit, onAction: (Action) -> Unit,
onTag: (GMTagItemUio.TagId) -> Unit,
) { ) {
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val startPadding = padding.calculateStartPadding(layoutDirection) val startPadding = padding.calculateStartPadding(layoutDirection)
Box( Box(
modifier = Modifier modifier = Modifier
.clip(shape = remember { RoundedCornerShape(8.dp) }) .clip(shape = MaterialTheme.lwa.shapes.gameMaster)
.clickable(onClick = onEdit) .clickable(onClick = onEdit)
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp) .background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.then(other = modifier), .then(other = modifier),
@ -109,6 +110,7 @@ fun GMCharacter(
Row( Row(
modifier = Modifier.padding(start = startPadding), modifier = Modifier.padding(start = startPadding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) { ) {
Row( Row(
modifier = Modifier.weight(weight = 1f), modifier = Modifier.weight(weight = 1f),
@ -133,6 +135,7 @@ fun GMCharacter(
GMTag( GMTag(
elevation = 4.dp, elevation = 4.dp,
tag = tag, tag = tag,
onTag = { onTag(tag.id) }
) )
} }
@ -147,7 +150,7 @@ fun GMCharacter(
@Composable @Composable
private fun OverflowActionMenu( private fun OverflowActionMenu(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
character: GMCharacterUio, character: GMCharacterItemUio,
onAction: (Action) -> Unit, onAction: (Action) -> Unit,
) { ) {
val overflowMenu = remember(character) { 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.runtime.Stable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
@Stable @Stable
data class GMTagUio( data class GMTagItemUio(
val id: TagId, val id: TagId,
val label: String, val label: String,
val highlight: Boolean, val highlight: Boolean,
@ -41,7 +40,7 @@ fun GMTag(
padding: PaddingValues = GmTagDefault.padding, padding: PaddingValues = GmTagDefault.padding,
shape: Shape = CircleShape, shape: Shape = CircleShape,
elevation: Dp = 2.dp, elevation: Dp = 2.dp,
tag: GMTagUio, tag: GMTagItemUio,
onTag: (() -> Unit)? = null, onTag: (() -> Unit)? = null,
) { ) {
val animatedColor = animateColorAsState( 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 portrait: Shape,
val panel: Shape, val panel: Shape,
val settings: Shape, val settings: Shape,
val gameMaster: Shape,
) )
@Stable @Stable
@ -20,10 +21,12 @@ fun lwaShapes(
portrait: Shape = RoundedCornerShape(8.dp), portrait: Shape = RoundedCornerShape(8.dp),
panel: Shape = RoundedCornerShape(8.dp), panel: Shape = RoundedCornerShape(8.dp),
settings: Shape = RoundedCornerShape(8.dp), settings: Shape = RoundedCornerShape(8.dp),
gameMaster: Shape = RoundedCornerShape(8.dp),
): LwaShapes = remember { ): LwaShapes = remember {
LwaShapes( LwaShapes(
portrait = portrait, portrait = portrait,
panel = panel, panel = panel,
settings = settings, settings = settings,
gameMaster = gameMaster,
) )
} }