Add chat to the campaign screen + change a bit the network UI.

This commit is contained in:
Thomas Andres Gomez 2025-03-03 17:15:15 +01:00
parent 3f67e342a7
commit 7a9dd97123
29 changed files with 885 additions and 100 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,840q-42,0 -71,-29t-29,-71q0,-42 29,-71t71,-29q42,0 71,29t29,71q0,42 -29,71t-71,29ZM254,614l-84,-86q59,-59 138.5,-93.5T480,400q92,0 171.5,35T790,530l-84,84q-44,-44 -102,-69t-124,-25q-66,0 -124,25t-102,69ZM84,444 L0,360q92,-94 215,-147t265,-53q142,0 265,53t215,147l-84,84q-77,-77 -178.5,-120.5T480,280q-116,0 -217.5,43.5T84,444Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M790,904 L414,526q-47,11 -87.5,33T254,614l-84,-86q32,-32 69,-56t79,-42l-90,-90q-41,21 -76.5,46.5T84,444L0,358q32,-32 66.5,-57.5T140,252l-84,-84 56,-56 736,736 -58,56ZM480,840q-42,0 -71,-29.5T380,740q0,-42 29,-71t71,-29q42,0 71,29t29,71q0,41 -29,70.5T480,840ZM716,602 L687,573 658,544 514,400q81,8 151.5,41T790,528l-74,74ZM876,444q-77,-77 -178.5,-120.5T480,280q-21,0 -40.5,1.5T400,286L298,184q44,-12 89.5,-18t92.5,-6q142,0 265,53t215,145l-84,86Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -9,6 +9,7 @@
<string name="main_page__open_save_directory">Ouvrir le dossier de sauvegarde</string> <string name="main_page__open_save_directory">Ouvrir le dossier de sauvegarde</string>
<string name="main_page__roll_history_action">Historique des lancers</string> <string name="main_page__roll_history_action">Historique des lancers</string>
<string name="main_page__network_action">Configuration de la table</string> <string name="main_page__network_action">Configuration de la table</string>
<string name="main_page__settings_action">Configuration de l'application</string>
<string name="roll_page__roll__label">Jet de :</string> <string name="roll_page__roll__label">Jet de :</string>
<string name="roll_page__roll__success_label">Réussite si lancer inférieur ou égal à : %1$s</string> <string name="roll_page__roll__success_label">Réussite si lancer inférieur ou égal à : %1$s</string>

View file

@ -28,6 +28,8 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.min
import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
@ -41,6 +43,7 @@ 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
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindowState
import com.pixelized.desktop.lwa.ui.navigation.window.WindowController import com.pixelized.desktop.lwa.ui.navigation.window.WindowController
import com.pixelized.desktop.lwa.ui.navigation.window.WindowsNavHost import com.pixelized.desktop.lwa.ui.navigation.window.WindowsNavHost
import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetEditWindow import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetEditWindow
@ -49,6 +52,8 @@ import com.pixelized.desktop.lwa.ui.navigation.window.destination.NetworkWindows
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.screen.campaign.CampaignViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon
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.network.NetworkPage import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
@ -78,15 +83,27 @@ val LocalErrorSnackHost = compositionLocalOf<SnackbarHostState> {
error("Local Snack Controller is not yet ready") error("Local Snack Controller is not yet ready")
} }
val LocalApplicationScope = compositionLocalOf<ApplicationScope> {
error("Local application scope is not yet ready")
}
@Composable @Composable
@Preview @Preview
fun ApplicationScope.App() { fun ApplicationScope.App() {
val maxWindowHeight = rememberMaxWindowHeight() val maxWindowHeight = rememberMaxWindowHeight()
val snackHostState = remember { SnackbarHostState() } val snackHostState = remember { SnackbarHostState() }
val errorSnackHostState = 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 windowsState = rememberWindowState(
size = DpSize(
width = 800.dp,
height = min(
a = 56.dp + PlayerRibbon.Default.size.height * 6 + 8.dp * 7 + 40.dp,
b = maxWindowHeight,
),
),
)
// Coil configuration // Coil configuration
setSingletonImageLoaderFactory { context -> setSingletonImageLoaderFactory { context ->
@ -96,14 +113,16 @@ fun ApplicationScope.App() {
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalApplicationScope provides this,
LocalSnackHost provides snackHostState, LocalSnackHost provides snackHostState,
LocalErrorSnackHost provides errorSnackHostState, LocalErrorSnackHost provides errorSnackHostState,
LocalWindowController provides windowController, LocalWindowController provides windowController,
LocalKeyEventHandlers provides keyEventHandlers, LocalKeyEventHandlers provides keyEventHandlers,
LocalWindowState provides windowsState,
) { ) {
Window( Window(
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
state = rememberWindowState(size = DpSize(width = 800.dp, height = maxWindowHeight)), state = windowsState,
title = runBlocking { getString(Res.string.app_name) }, title = runBlocking { getString(Res.string.app_name) },
onKeyEvent = { event -> onKeyEvent = { event ->
keyEventHandlers.reversed().any { it(event) } keyEventHandlers.reversed().any { it(event) }
@ -118,6 +137,7 @@ fun ApplicationScope.App() {
private fun MainWindowScreen( private fun MainWindowScreen(
campaignViewModel: CampaignViewModel = koinViewModel(), campaignViewModel: CampaignViewModel = koinViewModel(),
networkViewModel: NetworkViewModel = koinViewModel(), networkViewModel: NetworkViewModel = koinViewModel(),
campaignChatViewModel: CampaignChatViewModel = koinViewModel(),
rollViewModel: RollHistoryViewModel = koinViewModel(), rollViewModel: RollHistoryViewModel = koinViewModel(),
) { ) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -161,7 +181,11 @@ private fun MainWindowScreen(
} }
}, },
content = { content = {
MainNavHost() MainNavHost(
campaignViewModel = campaignViewModel,
networkViewModel = networkViewModel,
campaignChatViewModel = campaignChatViewModel,
)
} }
) )
NetworkSnackHandler( NetworkSnackHandler(

View file

@ -16,6 +16,8 @@ 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.screen.campaign.CampaignViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.TextMessageFactory
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.screen.campaign.player.detail.CharacterDiminishedViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
@ -105,6 +107,7 @@ val factoryDependencies
factoryOf(::PlayerRibbonFactory) factoryOf(::PlayerRibbonFactory)
factoryOf(::CharacterDetailFactory) factoryOf(::CharacterDetailFactory)
factoryOf(::CharacterSheetCharacteristicDialogFactory) factoryOf(::CharacterSheetCharacteristicDialogFactory)
factoryOf(::TextMessageFactory)
} }
val viewModelDependencies val viewModelDependencies
@ -120,6 +123,7 @@ val viewModelDependencies
viewModelOf(::CharacterDetailViewModel) viewModelOf(::CharacterDetailViewModel)
viewModelOf(::CharacterDiminishedViewModel) viewModelOf(::CharacterDiminishedViewModel)
viewModelOf(::CharacterDetailCharacteristicDialogViewModel) viewModelOf(::CharacterDetailCharacteristicDialogViewModel)
viewModelOf(::CampaignChatViewModel)
} }
val useCaseDependencies val useCaseDependencies

View file

@ -10,6 +10,10 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.destination.MainDestinatio
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableMainPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableMainPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableNetworkPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableNetworkPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableOldMainPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableOldMainPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableSettingsPage
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
val LocalScreenController = compositionLocalOf<NavHostController> { val LocalScreenController = compositionLocalOf<NavHostController> {
error("MainNavHost controller is not yet ready") error("MainNavHost controller is not yet ready")
@ -18,6 +22,9 @@ val LocalScreenController = compositionLocalOf<NavHostController> {
@Composable @Composable
fun MainNavHost( fun MainNavHost(
controller: NavHostController = rememberNavController(), controller: NavHostController = rememberNavController(),
campaignViewModel: CampaignViewModel,
networkViewModel: NetworkViewModel,
campaignChatViewModel: CampaignChatViewModel,
startDestination: String = MainDestination.navigationRoute(), startDestination: String = MainDestination.navigationRoute(),
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
@ -27,9 +34,14 @@ fun MainNavHost(
navController = controller, navController = controller,
startDestination = startDestination, startDestination = startDestination,
) { ) {
composableMainPage() composableMainPage(
campaignViewModel = campaignViewModel,
networkViewModel = networkViewModel,
campaignChatViewModel = campaignChatViewModel,
)
composableOldMainPage() composableOldMainPage()
composableNetworkPage() composableNetworkPage()
composableSettingsPage()
} }
} }
} }

View file

@ -3,7 +3,10 @@ package com.pixelized.desktop.lwa.ui.navigation.screen.destination
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.MainPage import com.pixelized.desktop.lwa.ui.screen.campaign.MainPage
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
object MainDestination { object MainDestination {
private const val ROUTE = "main" private const val ROUTE = "main"
@ -12,11 +15,19 @@ object MainDestination {
fun navigationRoute() = ROUTE fun navigationRoute() = ROUTE
} }
fun NavGraphBuilder.composableMainPage() { fun NavGraphBuilder.composableMainPage(
campaignViewModel: CampaignViewModel,
networkViewModel: NetworkViewModel,
campaignChatViewModel: CampaignChatViewModel,
) {
composable( composable(
route = MainDestination.baseRoute(), route = MainDestination.baseRoute(),
) { ) {
MainPage() MainPage(
campaignViewModel = campaignViewModel,
networkViewModel = networkViewModel,
campaignChatViewModel = campaignChatViewModel,
)
} }
} }

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.ui.navigation.screen.destination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.ui.screen.settings.SettingsScreen
object SettingsDestination {
private const val ROUTE = "settings"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableSettingsPage() {
composable(
route = SettingsDestination.baseRoute(),
) {
SettingsScreen()
}
}
fun NavHostController.navigateToSettings() {
val route = SettingsDestination.navigationRoute()
navigate(route = route)
}

View file

@ -10,12 +10,14 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight 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.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -36,22 +38,32 @@ import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentControlle
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.CharacterSheetCharacteristicDialog import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialog
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChat
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel
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.screen.campaign.player.detail.CharacterDiminishedViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.roll.RollPage import com.pixelized.desktop.lwa.ui.screen.roll.RollPage
import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
val LocalCampaignLayoutScope = compositionLocalOf<CampaignLayoutScope> {
error("LocalCampaignLayoutScope is not yet ready.")
}
@Composable @Composable
fun MainPage( fun MainPage(
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(), characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(), dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
campaignViewModel: CampaignViewModel = koinViewModel(),
networkViewModel: NetworkViewModel = koinViewModel(),
campaignChatViewModel: CampaignChatViewModel = koinViewModel(),
rollViewModel: RollViewModel = koinViewModel(), rollViewModel: RollViewModel = koinViewModel(),
) { ) {
KeyHandler { KeyHandler {
@ -78,20 +90,21 @@ fun MainPage(
CampaignScreenLayout( CampaignScreenLayout(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
top = { top = {
CampaignToolbar() CampaignToolbar(
campaignViewModel = campaignViewModel,
)
}, },
bottom = { bottom = {
Surface(
modifier = Modifier
// .height(48.dp)
.fillMaxWidth(),
elevation = 1.dp,
) {
}
}, },
main = { main = {
},
chat = {
CampaignChat(
modifier = Modifier.padding(all = 8.dp),
campaignChatViewModel = campaignChatViewModel,
)
}, },
leftOverlay = { leftOverlay = {
PlayerRibbon( PlayerRibbon(
@ -104,9 +117,9 @@ fun MainPage(
rightOverlay = { rightOverlay = {
CharacterDetailPanel( CharacterDetailPanel(
modifier = Modifier modifier = Modifier
.padding(all = 8.dp)
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight() .fillMaxHeight(),
.padding(all = 8.dp),
blurController = blurController, blurController = blurController,
detailViewModel = characterDetailViewModel, detailViewModel = characterDetailViewModel,
rollViewModel = rollViewModel, rollViewModel = rollViewModel,
@ -182,22 +195,27 @@ fun MainPage(
@Composable @Composable
private fun CampaignScreenLayout( private fun CampaignScreenLayout(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
top: @Composable CampaignScreenScope.() -> Unit, top: @Composable () -> Unit,
bottom: @Composable CampaignScreenScope.() -> Unit, bottom: @Composable () -> Unit,
main: @Composable CampaignScreenScope.() -> Unit, main: @Composable () -> Unit,
leftOverlay: @Composable CampaignScreenScope.() -> Unit, chat: @Composable () -> Unit,
rightOverlay: @Composable CampaignScreenScope.() -> Unit, leftOverlay: @Composable () -> Unit,
rightOverlay: @Composable () -> Unit,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val scope = remember { val scope = remember {
CampaignScreenScope( CampaignLayoutScope(
leftOverlay = leftOverlayState, leftOverlay = leftOverlayState,
rightOverlay = rightOverlayState, rightOverlay = rightOverlayState,
chatOverlay = chatOverlayState,
) )
} }
with(scope) { CompositionLocalProvider(
LocalCampaignLayoutScope provides scope,
) {
Column( Column(
modifier = modifier, modifier = modifier,
) { ) {
@ -212,6 +230,13 @@ private fun CampaignScreenLayout(
) { ) {
main() main()
} }
Box(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.onSizeChanged { }
) {
chat()
}
Box( Box(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.CenterStart) .align(alignment = Alignment.CenterStart)
@ -232,9 +257,11 @@ private fun CampaignScreenLayout(
} }
} }
private class CampaignScreenScope( @Stable
data class CampaignLayoutScope(
val leftOverlay: State<DpSize>, val leftOverlay: State<DpSize>,
val rightOverlay: State<DpSize>, val rightOverlay: State<DpSize>,
val chatOverlay: State<DpSize>,
) )
private fun IntSize.toDp(density: Density) = with(density) { private fun IntSize.toDp(density: Density) = with(density) {

View file

@ -22,6 +22,8 @@ class CampaignViewModel(
val title: Flow<String> = campaignRepository.campaignFlow val title: Flow<String> = campaignRepository.campaignFlow
.map { it.scene.name } .map { it.scene.name }
val networkStatus = network.status
fun init() { fun init() {
viewModelScope.launch { viewModelScope.launch {
combine( combine(

View file

@ -0,0 +1,147 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.chat
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.min
import androidx.compose.ui.window.WindowState
import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindowState
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignLayoutScope
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch
import org.koin.compose.viewmodel.koinViewModel
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CampaignChat(
modifier: Modifier = Modifier,
campaignChatViewModel: CampaignChatViewModel = koinViewModel(),
) {
val scope = rememberCoroutineScope()
val lazyState = rememberLazyListState()
val animatedChatWidth = rememberAnimatedChatWidth()
val colorScheme = MaterialTheme.lwa.colorScheme
val messages = campaignChatViewModel.messages.collectAsState()
ChatScrollDownEffect(
lazyState = lazyState,
messages = messages,
displayChat = campaignChatViewModel::displayChat,
hideChat = campaignChatViewModel::hideChat,
)
Box(
modifier = modifier
.size(
width = animatedChatWidth.value,
height = PlayerRibbon.Default.size.height * 2 + 8.dp,
)
.graphicsLayer {
alpha = campaignChatViewModel.chatAnimatedVisibility.value
}
.background(
shape = remember { RoundedCornerShape(8.dp) },
color = remember { colorScheme.elevated.base1dp.copy(alpha = 0.5f) },
)
.onPointerEvent(eventType = PointerEventType.Enter) {
scope.launch { campaignChatViewModel.displayChat() }
}
.onPointerEvent(eventType = PointerEventType.Exit) {
scope.launch { campaignChatViewModel.hideChat() }
},
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyState,
verticalArrangement = Arrangement.spacedBy(
space = 4.dp,
alignment = Alignment.Bottom,
),
contentPadding = remember { PaddingValues(all = 8.dp) },
) {
items(
items = messages.value,
key = { it.id },
contentType = { it.javaClass.simpleName }
) {
when (it) {
is RollTextMessageUio -> RollTextMessage(message = it)
}
}
}
}
}
@Composable
private fun ChatScrollDownEffect(
lazyState: LazyListState,
messages: State<List<TextMessage>>,
displayChat: suspend () -> Unit,
hideChat: suspend () -> Unit,
) {
LaunchedEffect(
key1 = messages.value.lastOrNull()?.id,
) {
if (messages.value.isNotEmpty()) {
displayChat()
lazyState.animateScrollToItem(
index = messages.value.lastIndex + 1,
)
hideChat()
}
}
}
@Composable
@Stable
private fun rememberAnimatedChatWidth(
campaignScreenScope: CampaignLayoutScope = LocalCampaignLayoutScope.current,
windowsState: WindowState = LocalWindowState.current,
): State<Dp> {
val chatWidth = remember(windowsState, campaignScreenScope) {
derivedStateOf {
val minChatWidth = 64.dp * 8
val maxChatWidth = 64.dp * 12
val windowWidth = windowsState.size.width
if (windowWidth != Dp.Unspecified) {
val width = windowWidth - campaignScreenScope.leftOverlay.value.width - 16.dp
min(max(width, minChatWidth), maxChatWidth)
} else {
minChatWidth
}
}
}
return animateDpAsState(targetValue = chatWidth.value)
}

View file

@ -0,0 +1,47 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.chat
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
class CampaignChatViewModel(
networkRepository: NetworkRepository,
textMessageFactory: TextMessageFactory,
) : ViewModel() {
val chatAnimatedVisibility = Animatable(0f)
private var _messages = emptyList<TextMessage>()
val messages: StateFlow<List<TextMessage>> = networkRepository.data
.mapNotNull { message ->
val text = textMessageFactory
.convertToTextMessage(message = message)
?: return@mapNotNull _messages
_messages = _messages.toMutableList().also {
it.add(index = it.lastIndex + 1, element = text)
}
_messages
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = _messages,
)
suspend fun displayChat() {
chatAnimatedVisibility.animateTo(1f)
}
suspend fun hideChat() {
chatAnimatedVisibility.animateTo(0f, animationSpec = tween(2000, delayMillis = 8000))
}
}

View file

@ -0,0 +1,59 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.chat
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
import java.text.SimpleDateFormat
class TextMessageFactory(
private val characterSheetRepository: CharacterSheetRepository,
) {
private val formatId = SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS")
private val formatTime = SimpleDateFormat("HH:mm:ss")
fun convertToTextMessage(
message: Message,
): TextMessage? {
val time = System.currentTimeMillis()
val id = formatId.format(time)
return when (val payload = message.value) {
is RollMessage -> {
val sheetPreview = characterSheetRepository
.characterPreview(characterId = payload.characterId)
?: return null
RollTextMessageUio(
id = id,
timestamp = formatTime.format(time),
character = sheetPreview.name,
skillLabel = payload.skillLabel,
rollDifficulty = payload.rollDifficulty,
rollValue = payload.rollValue,
rollSuccessLimit = payload.rollSuccessLimit,
resultLabel = payload.resultLabel,
resultType = when (payload.critical) {
RollMessage.Critical.CRITICAL_SUCCESS -> RollTextMessageUio.Critical.CRITICAL_SUCCESS
RollMessage.Critical.SPECIAL_SUCCESS -> RollTextMessageUio.Critical.SPECIAL_SUCCESS
RollMessage.Critical.SUCCESS -> RollTextMessageUio.Critical.SUCCESS
RollMessage.Critical.FAILURE -> RollTextMessageUio.Critical.FAILURE
RollMessage.Critical.CRITICAL_FAILURE -> RollTextMessageUio.Critical.CRITICAL_FAILURE
null -> null
}
)
}
is CampaignMessage.UpdateCharacteristic -> null
is CampaignMessage.UpdateDiminished -> null
RestSynchronisation.Campaign -> null
is RestSynchronisation.CharacterDelete -> null
is RestSynchronisation.CharacterUpdate -> null
is RestSynchronisation.ToggleActiveAlteration -> null
is UpdateSkillUsageMessage -> null
}
}
}

View file

@ -0,0 +1,153 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.roll_history__item__difficulty
import lwacharactersheet.composeapp.generated.resources.roll_history__item__throw
import org.jetbrains.compose.resources.stringResource
@Stable
data class RollTextMessageUio(
override val id: String,
override val timestamp: String,
val character: String,
val skillLabel: String,
val rollDifficulty: String?,
val rollValue: Int,
val rollSuccessLimit: Int?,
val resultLabel: String?,
val resultType: Critical?,
) : TextMessage {
enum class Critical {
CRITICAL_SUCCESS,
SPECIAL_SUCCESS,
SUCCESS,
FAILURE,
CRITICAL_FAILURE
}
}
@Composable
fun RollTextMessage(
modifier: Modifier = Modifier,
message: RollTextMessageUio,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 3.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.timestamp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = message.timestamp,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.timestamp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = ">",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = message.character,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
fontWeight = FontWeight.ExtraLight,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.roll_history__item__throw),
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = message.skillLabel,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "-",
)
message.resultLabel?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
color = when (message.resultType) {
RollTextMessageUio.Critical.CRITICAL_SUCCESS -> MaterialTheme.lwa.colorScheme.chat.criticalSuccess
RollTextMessageUio.Critical.SPECIAL_SUCCESS -> MaterialTheme.lwa.colorScheme.chat.spacialSuccess
RollTextMessageUio.Critical.SUCCESS -> MaterialTheme.lwa.colorScheme.chat.success
RollTextMessageUio.Critical.FAILURE -> MaterialTheme.lwa.colorScheme.chat.failure
RollTextMessageUio.Critical.CRITICAL_FAILURE -> MaterialTheme.lwa.colorScheme.chat.criticalFailure
null -> MaterialTheme.lwa.colorScheme.chat.text
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = it,
)
}
Row {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "${message.rollValue}",
)
message.rollSuccessLimit?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "/$it",
)
}
}
message.rollDifficulty?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "-",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.roll_history__item__difficulty),
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = it,
)
}
}
}

View file

@ -0,0 +1,6 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text
sealed interface TextMessage {
val id : String
val timestamp: String
}

View file

@ -118,6 +118,7 @@ fun CharacterDetailPanel(
blurController.show() blurController.show()
} }
) )
} }
@Composable @Composable

View file

@ -99,7 +99,8 @@ fun PlayerPortraitRoll(
.onClick( .onClick(
matcher = PointerMatcher.mouse(PointerButton.Secondary), matcher = PointerMatcher.mouse(PointerButton.Secondary),
onClick = { onRightClick(it) }, onClick = { onRightClick(it) },
).clickable { )
.clickable {
onLeftClick(it) onLeftClick(it)
} }
.padding(all = 8.dp), .padding(all = 8.dp),

View file

@ -1,6 +1,7 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon import androidx.compose.material.Icon
@ -16,19 +17,24 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToOldMainPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToOldMainPage
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToNetwork import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToSettings
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
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.ic_settings_24dp import lwacharactersheet.composeapp.generated.resources.ic_settings_24dp
import lwacharactersheet.composeapp.generated.resources.ic_table_24dp import lwacharactersheet.composeapp.generated.resources.ic_table_24dp
import lwacharactersheet.composeapp.generated.resources.ic_timeline_24dp import lwacharactersheet.composeapp.generated.resources.ic_timeline_24dp
import lwacharactersheet.composeapp.generated.resources.main_page__network_action import lwacharactersheet.composeapp.generated.resources.ic_wifi_24dp
import lwacharactersheet.composeapp.generated.resources.ic_wifi_off_24dp
import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@ -37,30 +43,76 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
fun CampaignToolbar( fun CampaignToolbar(
campaignViewModel: CampaignViewModel = koinViewModel(), campaignViewModel: CampaignViewModel = koinViewModel(),
networkViewModel: NetworkViewModel = koinViewModel(),
) { ) {
val windows = LocalWindowController.current val windows = LocalWindowController.current
val screen = LocalScreenController.current val screen = LocalScreenController.current
val isOverflowMenuOpen = remember { mutableStateOf(false) } val isOverflowMenuOpen = remember { mutableStateOf(false) }
val isNetworkMenuOpen = remember { mutableStateOf(false) }
CampaignToolbarContent( CampaignToolbarContent(
title = campaignViewModel.title.collectAsState(initial = ""), title = campaignViewModel.title.collectAsState(initial = ""),
networkStatus = campaignViewModel.networkStatus.collectAsState(),
isNetworkMenuOpen = isNetworkMenuOpen,
isOverflowMenuOpen = isOverflowMenuOpen, isOverflowMenuOpen = isOverflowMenuOpen,
networkMenu = {
NetworkPage(
modifier = Modifier.size(384.dp, 240.dp),
viewModel = networkViewModel
)
},
overflowMenu = {
DropdownMenuItem(
onClick = {
isOverflowMenuOpen.value = false
windows.navigateToRollHistory()
},
) {
Icon(
painter = painterResource(Res.drawable.ic_timeline_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
modifier = Modifier.padding(start = 8.dp),
color = MaterialTheme.colors.primary,
text = stringResource(Res.string.main_page__roll_history_action),
)
}
DropdownMenuItem(
onClick = {
isOverflowMenuOpen.value = false
screen.navigateToOldMainPage()
},
) {
Icon(
painter = painterResource(Res.drawable.ic_table_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
modifier = Modifier.padding(start = 8.dp),
color = MaterialTheme.colors.primary,
text = "Ancienne interface utilisateur",
)
}
},
onNetwork = {
isNetworkMenuOpen.value = true
},
onOverflow = { onOverflow = {
isOverflowMenuOpen.value = isOverflowMenuOpen.value.not() isOverflowMenuOpen.value = isOverflowMenuOpen.value.not()
}, },
onSettings = {
screen.navigateToSettings()
},
onDismissNetworkMenu = {
isNetworkMenuOpen.value = false
},
onDismissOverflowMenu = { onDismissOverflowMenu = {
isOverflowMenuOpen.value = false isOverflowMenuOpen.value = false
}, },
onRollHistory = {
windows.navigateToRollHistory()
},
onNetwork = {
windows.navigateToNetwork()
},
onOlUi = {
screen.navigateToOldMainPage()
},
) )
} }
@ -68,12 +120,16 @@ fun CampaignToolbar(
private fun CampaignToolbarContent( private fun CampaignToolbarContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: State<String>, title: State<String>,
networkStatus: State<NetworkRepository.Status>,
isNetworkMenuOpen: State<Boolean>,
isOverflowMenuOpen: State<Boolean>, isOverflowMenuOpen: State<Boolean>,
onOverflow: () -> Unit, networkMenu: @Composable () -> Unit,
onDismissOverflowMenu: () -> Unit, overflowMenu: @Composable () -> Unit,
onRollHistory: () -> Unit,
onNetwork: () -> Unit, onNetwork: () -> Unit,
onOlUi: () -> Unit, onOverflow: () -> Unit,
onSettings: () -> Unit,
onDismissNetworkMenu: () -> Unit,
onDismissOverflowMenu: () -> Unit,
) { ) {
TopAppBar( TopAppBar(
modifier = modifier, modifier = modifier,
@ -83,6 +139,38 @@ private fun CampaignToolbarContent(
) )
}, },
actions = { actions = {
IconButton(
onClick = onNetwork
) {
Icon(
painter = painterResource(
when (networkStatus.value) {
NetworkRepository.Status.CONNECTED -> Res.drawable.ic_wifi_24dp
NetworkRepository.Status.DISCONNECTED -> Res.drawable.ic_wifi_off_24dp
}
),
tint = when (networkStatus.value) {
NetworkRepository.Status.CONNECTED -> MaterialTheme.lwa.colorScheme.base.primary
NetworkRepository.Status.DISCONNECTED -> MaterialTheme.lwa.colorScheme.base.error
},
contentDescription = null,
)
}
DropdownMenu(
offset = remember { DpOffset(x = -(48.dp * 2 + 8.dp), y = 8.dp) },
expanded = isNetworkMenuOpen.value,
onDismissRequest = onDismissNetworkMenu,
content = { networkMenu() },
)
IconButton(
onClick = onSettings
) {
Icon(
painter = painterResource(Res.drawable.ic_settings_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
IconButton( IconButton(
onClick = onOverflow, onClick = onOverflow,
) { ) {
@ -92,54 +180,12 @@ private fun CampaignToolbarContent(
contentDescription = null, contentDescription = null,
) )
} }
DropdownMenu( DropdownMenu(
offset = remember { DpOffset(x = (-8).dp, y = 8.dp) },
expanded = isOverflowMenuOpen.value, expanded = isOverflowMenuOpen.value,
onDismissRequest = onDismissOverflowMenu, onDismissRequest = onDismissOverflowMenu,
) { content = { overflowMenu() },
DropdownMenuItem( )
onClick = { onDismissOverflowMenu(); onRollHistory() },
) {
Icon(
painter = painterResource(Res.drawable.ic_timeline_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
modifier = Modifier.padding(start = 8.dp),
color = MaterialTheme.colors.primary,
text = stringResource(Res.string.main_page__roll_history_action),
)
}
DropdownMenuItem(
onClick = { onDismissOverflowMenu(); onNetwork() },
) {
Icon(
painter = painterResource(Res.drawable.ic_settings_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
modifier = Modifier.padding(start = 8.dp),
color = MaterialTheme.colors.primary,
text = stringResource(Res.string.main_page__network_action),
)
}
DropdownMenuItem(
onClick = { onDismissOverflowMenu(); onOlUi() },
) {
Icon(
painter = painterResource(Res.drawable.ic_table_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
modifier = Modifier.padding(start = 8.dp),
color = MaterialTheme.colors.primary,
text = "Ancienne interface utilisateur",
)
}
}
}, },
) )
} }

View file

@ -64,6 +64,7 @@ data class NetworkPageUio(
val enableCancel: Boolean, val enableCancel: Boolean,
) { ) {
companion object { companion object {
@Stable
fun empty( fun empty(
player: String = "", player: String = "",
host: String = "", host: String = "",
@ -167,11 +168,14 @@ fun NetworkScreen(
@Composable @Composable
fun NetworkPage( fun NetworkPage(
modifier: Modifier = Modifier,
viewModel: NetworkViewModel = koinViewModel(), viewModel: NetworkViewModel = koinViewModel(),
) { ) {
val snack = LocalSnackHost.current val snack = LocalSnackHost.current
Surface { Surface(
modifier = modifier,
) {
BlurContent( BlurContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
controller = viewModel.blurController, controller = viewModel.blurController,
@ -267,6 +271,7 @@ private fun NetworkContent(
} }
TextButton( TextButton(
modifier = Modifier.align(alignment = Alignment.End),
enabled = player.value.enableActions, enabled = player.value.enableActions,
onClick = onConnect, onClick = onConnect,
) { ) {
@ -274,6 +279,7 @@ private fun NetworkContent(
} }
TextButton( TextButton(
modifier = Modifier.align(alignment = Alignment.End),
enabled = player.value.enableCancel, enabled = player.value.enableCancel,
onClick = onDisconnect, onClick = onDisconnect,
) { ) {

View file

@ -64,11 +64,11 @@ class NetworkViewModel(
} }
fun onPortChange(port: String) { fun onPortChange(port: String) {
this.portFlow.value = port.toIntOrNull() ?: settings.port portFlow.value = port.toIntOrNull() ?: settings.port
} }
fun onHostChange(host: String) { fun onHostChange(host: String) {
this.hostFlow.value = host hostFlow.value = host
} }
fun connect() { fun connect() {

View file

@ -196,6 +196,16 @@ class RollViewModel(
rollValue = roll, rollValue = roll,
rollSuccessLimit = rollStep?.success?.last, rollSuccessLimit = rollStep?.success?.last,
resultLabel = success, resultLabel = success,
critical = rollStep?.let {
when (roll) {
in it.criticalSuccess -> RollMessage.Critical.CRITICAL_SUCCESS
in it.specialSuccess -> RollMessage.Critical.SPECIAL_SUCCESS
in it.success -> RollMessage.Critical.SUCCESS
in it.failure -> RollMessage.Critical.FAILURE
in it.criticalFailure -> RollMessage.Critical.CRITICAL_FAILURE
else -> null
}
}
) )
networkRepository.share( networkRepository.share(
payload = payload, payload = payload,

View file

@ -0,0 +1,61 @@
package com.pixelized.desktop.lwa.ui.screen.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
@Composable
fun SettingsScreen() {
val screen = LocalScreenController.current
Surface {
SettingsContent(
modifier = Modifier.fillMaxSize(),
onBack = {
screen.popBackStack()
},
)
}
}
@Composable
private fun SettingsContent(
modifier: Modifier = Modifier,
onBack: () -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
title = { },
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues = paddingValues),
) {
}
}
)
}

View file

@ -6,8 +6,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.pixelized.desktop.lwa.ui.theme.color.LwaColorTheme import com.pixelized.desktop.lwa.ui.theme.color.LwaColors
import com.pixelized.desktop.lwa.ui.theme.color.darkLwaColorTheme import com.pixelized.desktop.lwa.ui.theme.color.darkLwaColorTheme
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
import com.pixelized.desktop.lwa.ui.theme.typography.lwaTypography
val LocalLwaTheme = compositionLocalOf<LwaTheme> { val LocalLwaTheme = compositionLocalOf<LwaTheme> {
error("Local Snack Controller is not yet ready") error("Local Snack Controller is not yet ready")
@ -20,17 +22,21 @@ val MaterialTheme.lwa: LwaTheme
@Stable @Stable
data class LwaTheme( data class LwaTheme(
val colorScheme: LwaColorTheme, val colorScheme: LwaColors,
val typography: LwaTypography,
) )
@Composable @Composable
fun LwaTheme( fun LwaTheme(
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val lwaColorTheme = darkLwaColorTheme() val lwaColors = darkLwaColorTheme()
val lwaTypography = lwaTypography(colors = lwaColors)
val theme = remember { val theme = remember {
LwaTheme( LwaTheme(
colorScheme = lwaColorTheme, colorScheme = lwaColors,
typography = lwaTypography,
) )
} }
@ -38,7 +44,7 @@ fun LwaTheme(
LocalLwaTheme provides theme LocalLwaTheme provides theme
) { ) {
MaterialTheme( MaterialTheme(
colors = lwaColorTheme.base, colors = lwaColors.base,
typography = MaterialTheme.typography, typography = MaterialTheme.typography,
shapes = MaterialTheme.shapes, shapes = MaterialTheme.shapes,
content = content, content = content,

View file

@ -4,4 +4,10 @@ import androidx.compose.ui.graphics.Color
object LwaColorPalette { object LwaColorPalette {
val DefaultScrimColor = Color.Black.copy(alpha = 0.4f) val DefaultScrimColor = Color.Black.copy(alpha = 0.4f)
val Orange400 = Color(0xFFFFA726)
val Red400 = Color(0xFFFF7043)
val LightGreen400 = Color(0xFF9CCC65)
val Green400 = Color(0xFF66BB6A)
val Teal400 = Color(0xFF26A69A)
} }

View file

@ -13,15 +13,29 @@ import androidx.compose.ui.unit.dp
import kotlin.math.ln import kotlin.math.ln
@Stable @Stable
data class LwaColorTheme( data class LwaColors(
val base: Colors, val base: Colors,
val elevated: Elevated, val elevated: Elevated,
val portraitBackgroundBrush: Brush, val portraitBackgroundBrush: Brush,
val chat: Chat,
) { ) {
@Stable @Stable
data class Elevated( data class Elevated(
val base1dp: Color, val base1dp: Color,
val base2dp: Color, val base2dp: Color,
val base3dp: Color,
val base4dp: Color,
)
@Stable
data class Chat(
val timestamp: Color,
val text: Color,
val criticalSuccess: Color,
val spacialSuccess: Color,
val success: Color,
val failure: Color,
val criticalFailure: Color,
) )
} }
@ -29,7 +43,7 @@ data class LwaColorTheme(
@Stable @Stable
fun darkLwaColorTheme( fun darkLwaColorTheme(
base: Colors = darkColors(), base: Colors = darkColors(),
elevated: LwaColorTheme.Elevated = LwaColorTheme.Elevated( elevated: LwaColors.Elevated = LwaColors.Elevated(
base1dp = base.calculateElevatedColor( base1dp = base.calculateElevatedColor(
color = base.surface, color = base.surface,
onColor = base.onSurface, onColor = base.onSurface,
@ -40,6 +54,16 @@ fun darkLwaColorTheme(
onColor = base.onSurface, onColor = base.onSurface,
elevation = 2.dp, elevation = 2.dp,
), ),
base3dp = base.calculateElevatedColor(
color = base.surface,
onColor = base.onSurface,
elevation = 3.dp,
),
base4dp = base.calculateElevatedColor(
color = base.surface,
onColor = base.onSurface,
elevation = 4.dp,
),
), ),
portraitBackgroundBrush: Brush = Brush.verticalGradient( portraitBackgroundBrush: Brush = Brush.verticalGradient(
listOf( listOf(
@ -49,11 +73,21 @@ fun darkLwaColorTheme(
elevated.base1dp.copy(alpha = 0.5f), elevated.base1dp.copy(alpha = 0.5f),
elevated.base1dp.copy(alpha = 0.8f), elevated.base1dp.copy(alpha = 0.8f),
) )
) ),
): LwaColorTheme = LwaColorTheme( chat: LwaColors.Chat = LwaColors.Chat(
timestamp = base.secondary,
text = base.onSurface.copy(alpha = 0.7f),
criticalSuccess = LwaColorPalette.Teal400,
spacialSuccess = LwaColorPalette.Green400,
success = LwaColorPalette.LightGreen400,
failure = LwaColorPalette.Orange400,
criticalFailure = LwaColorPalette.Red400,
),
): LwaColors = LwaColors(
base = base, base = base,
elevated = elevated, elevated = elevated,
portraitBackgroundBrush = portraitBackgroundBrush, portraitBackgroundBrush = portraitBackgroundBrush,
chat = chat,
) )
@ReadOnlyComposable @ReadOnlyComposable

View file

@ -0,0 +1,68 @@
package com.pixelized.desktop.lwa.ui.theme.typography
import androidx.compose.material.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.pixelized.desktop.lwa.ui.theme.color.LwaColors
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.consola_mono_bold
import lwacharactersheet.composeapp.generated.resources.consola_mono_book
import org.jetbrains.compose.resources.Font
@Stable
data class LwaTypography(
val base: Typography,
val chat: Chat,
) {
@Stable
data class Chat(
val timestamp: TextStyle,
val text: TextStyle,
)
}
@Composable
@Stable
fun ConsolaFontFamily() = FontFamily(
Font(resource = Res.font.consola_mono_book, weight = FontWeight.Normal),
Font(resource = Res.font.consola_mono_bold, weight = FontWeight.Bold),
)
@Composable
@Stable
fun lwaTypography(
base: Typography = Typography(),
colors: LwaColors,
): LwaTypography {
val jack = ConsolaFontFamily()
return remember(
jack,
) {
LwaTypography(
base = base,
chat = LwaTypography.Chat(
timestamp = base.body1.copy(
fontFamily = jack,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.sp,
color = colors.chat.timestamp,
),
text = base.body1.copy(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp,
color = colors.chat.text,
),
)
)
}
}

View file

@ -10,4 +10,13 @@ data class RollMessage(
val rollDifficulty: String? = null, val rollDifficulty: String? = null,
val rollValue: Int, val rollValue: Int,
val rollSuccessLimit: Int? = null, val rollSuccessLimit: Int? = null,
) : MessagePayload val critical: Critical?,
) : MessagePayload {
enum class Critical {
CRITICAL_SUCCESS,
SPECIAL_SUCCESS,
SUCCESS,
FAILURE,
CRITICAL_FAILURE
}
}