diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_wifi_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_wifi_24dp.xml new file mode 100644 index 0000000..261089d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_wifi_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_wifi_off_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_wifi_off_24dp.xml new file mode 100644 index 0000000..facc091 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_wifi_off_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/font/consola_mono_bold.ttf b/composeApp/src/commonMain/composeResources/font/consola_mono_bold.ttf new file mode 100644 index 0000000..7e289b3 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/consola_mono_bold.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/consola_mono_book.ttf b/composeApp/src/commonMain/composeResources/font/consola_mono_book.ttf new file mode 100644 index 0000000..78ca9da Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/consola_mono_book.ttf differ diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 7c6d410..f17faa1 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -9,6 +9,7 @@ Ouvrir le dossier de sauvegarde Historique des lancers Configuration de la table + Configuration de l'application Jet de : Réussite si lancer inférieur ou égal à : %1$s diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt index 1de11d9..10bc5ac 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt @@ -28,6 +28,8 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize 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.Window 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.destination.CharacterSheetDestination 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.WindowsNavHost 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.rememberMaxWindowHeight 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.network.NetworkPage import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel @@ -78,15 +83,27 @@ val LocalErrorSnackHost = compositionLocalOf { error("Local Snack Controller is not yet ready") } +val LocalApplicationScope = compositionLocalOf { + error("Local application scope is not yet ready") +} + @Composable @Preview fun ApplicationScope.App() { - val maxWindowHeight = rememberMaxWindowHeight() val snackHostState = remember { SnackbarHostState() } val errorSnackHostState = remember { SnackbarHostState() } val windowController = remember { WindowController(maxWindowHeight) } val keyEventHandlers = remember { mutableStateListOf() } + 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 setSingletonImageLoaderFactory { context -> @@ -96,14 +113,16 @@ fun ApplicationScope.App() { } CompositionLocalProvider( + LocalApplicationScope provides this, LocalSnackHost provides snackHostState, LocalErrorSnackHost provides errorSnackHostState, LocalWindowController provides windowController, LocalKeyEventHandlers provides keyEventHandlers, + LocalWindowState provides windowsState, ) { Window( onCloseRequest = ::exitApplication, - state = rememberWindowState(size = DpSize(width = 800.dp, height = maxWindowHeight)), + state = windowsState, title = runBlocking { getString(Res.string.app_name) }, onKeyEvent = { event -> keyEventHandlers.reversed().any { it(event) } @@ -118,6 +137,7 @@ fun ApplicationScope.App() { private fun MainWindowScreen( campaignViewModel: CampaignViewModel = koinViewModel(), networkViewModel: NetworkViewModel = koinViewModel(), + campaignChatViewModel: CampaignChatViewModel = koinViewModel(), rollViewModel: RollHistoryViewModel = koinViewModel(), ) { LaunchedEffect(Unit) { @@ -161,7 +181,11 @@ private fun MainWindowScreen( } }, content = { - MainNavHost() + MainNavHost( + campaignViewModel = campaignViewModel, + networkViewModel = networkViewModel, + campaignChatViewModel = campaignChatViewModel, + ) } ) NetworkSnackHandler( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index e020579..c509289 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -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.CharacterSheetCharacteristicDialogFactory 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.CharacterDetailViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel @@ -105,6 +107,7 @@ val factoryDependencies factoryOf(::PlayerRibbonFactory) factoryOf(::CharacterDetailFactory) factoryOf(::CharacterSheetCharacteristicDialogFactory) + factoryOf(::TextMessageFactory) } val viewModelDependencies @@ -120,6 +123,7 @@ val viewModelDependencies viewModelOf(::CharacterDetailViewModel) viewModelOf(::CharacterDiminishedViewModel) viewModelOf(::CharacterDetailCharacteristicDialogViewModel) + viewModelOf(::CampaignChatViewModel) } val useCaseDependencies diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt index 0103989..2fd1f09 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt @@ -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.composableNetworkPage 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 { error("MainNavHost controller is not yet ready") @@ -18,6 +22,9 @@ val LocalScreenController = compositionLocalOf { @Composable fun MainNavHost( controller: NavHostController = rememberNavController(), + campaignViewModel: CampaignViewModel, + networkViewModel: NetworkViewModel, + campaignChatViewModel: CampaignChatViewModel, startDestination: String = MainDestination.navigationRoute(), ) { CompositionLocalProvider( @@ -27,9 +34,14 @@ fun MainNavHost( navController = controller, startDestination = startDestination, ) { - composableMainPage() + composableMainPage( + campaignViewModel = campaignViewModel, + networkViewModel = networkViewModel, + campaignChatViewModel = campaignChatViewModel, + ) composableOldMainPage() composableNetworkPage() + composableSettingsPage() } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt index dc0dd26..f6d5817 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt @@ -3,7 +3,10 @@ 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.campaign.CampaignViewModel 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 { private const val ROUTE = "main" @@ -12,11 +15,19 @@ object MainDestination { fun navigationRoute() = ROUTE } -fun NavGraphBuilder.composableMainPage() { +fun NavGraphBuilder.composableMainPage( + campaignViewModel: CampaignViewModel, + networkViewModel: NetworkViewModel, + campaignChatViewModel: CampaignChatViewModel, +) { composable( route = MainDestination.baseRoute(), ) { - MainPage() + MainPage( + campaignViewModel = campaignViewModel, + networkViewModel = networkViewModel, + campaignChatViewModel = campaignChatViewModel, + ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/SettingsDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/SettingsDestination.kt new file mode 100644 index 0000000..eda1551 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/SettingsDestination.kt @@ -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) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index e04a78d..f6afe13 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -10,12 +10,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable import androidx.compose.runtime.State +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.CharacterSheetCharacteristicDialog 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.CharacterDetailViewModel 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.toolbar.CampaignToolbar 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.RollViewModel import kotlinx.coroutines.launch import org.koin.compose.viewmodel.koinViewModel +val LocalCampaignLayoutScope = compositionLocalOf { + error("LocalCampaignLayoutScope is not yet ready.") +} + @Composable fun MainPage( characterDetailViewModel: CharacterDetailViewModel = koinViewModel(), characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(), + campaignViewModel: CampaignViewModel = koinViewModel(), + networkViewModel: NetworkViewModel = koinViewModel(), + campaignChatViewModel: CampaignChatViewModel = koinViewModel(), rollViewModel: RollViewModel = koinViewModel(), ) { KeyHandler { @@ -64,7 +76,7 @@ fun MainPage( else -> false } } - + val scope = rememberCoroutineScope() val blurController = rememberBlurContentController() @@ -78,20 +90,21 @@ fun MainPage( CampaignScreenLayout( modifier = Modifier.fillMaxSize(), top = { - CampaignToolbar() + CampaignToolbar( + campaignViewModel = campaignViewModel, + ) }, bottom = { - Surface( - modifier = Modifier -// .height(48.dp) - .fillMaxWidth(), - elevation = 1.dp, - ) { - } }, main = { + }, + chat = { + CampaignChat( + modifier = Modifier.padding(all = 8.dp), + campaignChatViewModel = campaignChatViewModel, + ) }, leftOverlay = { PlayerRibbon( @@ -104,9 +117,9 @@ fun MainPage( rightOverlay = { CharacterDetailPanel( modifier = Modifier + .padding(all = 8.dp) .width(width = 128.dp * 4) - .fillMaxHeight() - .padding(all = 8.dp), + .fillMaxHeight(), blurController = blurController, detailViewModel = characterDetailViewModel, rollViewModel = rollViewModel, @@ -182,22 +195,27 @@ fun MainPage( @Composable private fun CampaignScreenLayout( modifier: Modifier = Modifier, - top: @Composable CampaignScreenScope.() -> Unit, - bottom: @Composable CampaignScreenScope.() -> Unit, - main: @Composable CampaignScreenScope.() -> Unit, - leftOverlay: @Composable CampaignScreenScope.() -> Unit, - rightOverlay: @Composable CampaignScreenScope.() -> Unit, + top: @Composable () -> Unit, + bottom: @Composable () -> Unit, + main: @Composable () -> Unit, + chat: @Composable () -> Unit, + leftOverlay: @Composable () -> Unit, + rightOverlay: @Composable () -> Unit, ) { val density = LocalDensity.current val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) } + val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val scope = remember { - CampaignScreenScope( + CampaignLayoutScope( leftOverlay = leftOverlayState, rightOverlay = rightOverlayState, + chatOverlay = chatOverlayState, ) } - with(scope) { + CompositionLocalProvider( + LocalCampaignLayoutScope provides scope, + ) { Column( modifier = modifier, ) { @@ -212,6 +230,13 @@ private fun CampaignScreenLayout( ) { main() } + Box( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .onSizeChanged { } + ) { + chat() + } Box( modifier = Modifier .align(alignment = Alignment.CenterStart) @@ -232,9 +257,11 @@ private fun CampaignScreenLayout( } } -private class CampaignScreenScope( +@Stable +data class CampaignLayoutScope( val leftOverlay: State, val rightOverlay: State, + val chatOverlay: State, ) private fun IntSize.toDp(density: Density) = with(density) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignViewModel.kt index c901665..02447fa 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignViewModel.kt @@ -22,6 +22,8 @@ class CampaignViewModel( val title: Flow = campaignRepository.campaignFlow .map { it.scene.name } + val networkStatus = network.status + fun init() { viewModelScope.launch { combine( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChat.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChat.kt new file mode 100644 index 0000000..13e4fa3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChat.kt @@ -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>, + 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 { + 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) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChatViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChatViewModel.kt new file mode 100644 index 0000000..160843a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChatViewModel.kt @@ -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() + val messages: StateFlow> = 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)) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt new file mode 100644 index 0000000..bc8d5df --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/RollText.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/RollText.kt new file mode 100644 index 0000000..e3c9954 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/RollText.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/TextMessage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/TextMessage.kt new file mode 100644 index 0000000..9ede501 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/TextMessage.kt @@ -0,0 +1,6 @@ +package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text + +sealed interface TextMessage { + val id : String + val timestamp: String +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt index 4a64c17..22f5bd0 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt @@ -118,6 +118,7 @@ fun CharacterDetailPanel( blurController.show() } ) + } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortraitRoll.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortraitRoll.kt index 56e4199..4966f6b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortraitRoll.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortraitRoll.kt @@ -99,7 +99,8 @@ fun PlayerPortraitRoll( .onClick( matcher = PointerMatcher.mouse(PointerButton.Secondary), onClick = { onRightClick(it) }, - ).clickable { + ) + .clickable { onLeftClick(it) } .padding(all = 8.dp), diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt index d301dce..809d2d9 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt @@ -1,6 +1,7 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon @@ -16,19 +17,24 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp 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.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.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 lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_settings_24dp import lwacharactersheet.composeapp.generated.resources.ic_table_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 org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -37,30 +43,76 @@ import org.koin.compose.viewmodel.koinViewModel @Composable fun CampaignToolbar( campaignViewModel: CampaignViewModel = koinViewModel(), + networkViewModel: NetworkViewModel = koinViewModel(), ) { val windows = LocalWindowController.current val screen = LocalScreenController.current val isOverflowMenuOpen = remember { mutableStateOf(false) } + val isNetworkMenuOpen = remember { mutableStateOf(false) } CampaignToolbarContent( title = campaignViewModel.title.collectAsState(initial = ""), + networkStatus = campaignViewModel.networkStatus.collectAsState(), + isNetworkMenuOpen = isNetworkMenuOpen, 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 = { isOverflowMenuOpen.value = isOverflowMenuOpen.value.not() }, + onSettings = { + screen.navigateToSettings() + }, + onDismissNetworkMenu = { + isNetworkMenuOpen.value = false + }, onDismissOverflowMenu = { isOverflowMenuOpen.value = false }, - onRollHistory = { - windows.navigateToRollHistory() - }, - onNetwork = { - windows.navigateToNetwork() - }, - onOlUi = { - screen.navigateToOldMainPage() - }, ) } @@ -68,12 +120,16 @@ fun CampaignToolbar( private fun CampaignToolbarContent( modifier: Modifier = Modifier, title: State, + networkStatus: State, + isNetworkMenuOpen: State, isOverflowMenuOpen: State, - onOverflow: () -> Unit, - onDismissOverflowMenu: () -> Unit, - onRollHistory: () -> Unit, + networkMenu: @Composable () -> Unit, + overflowMenu: @Composable () -> Unit, onNetwork: () -> Unit, - onOlUi: () -> Unit, + onOverflow: () -> Unit, + onSettings: () -> Unit, + onDismissNetworkMenu: () -> Unit, + onDismissOverflowMenu: () -> Unit, ) { TopAppBar( modifier = modifier, @@ -83,6 +139,38 @@ private fun CampaignToolbarContent( ) }, 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( onClick = onOverflow, ) { @@ -92,54 +180,12 @@ private fun CampaignToolbarContent( contentDescription = null, ) } - DropdownMenu( + offset = remember { DpOffset(x = (-8).dp, y = 8.dp) }, expanded = isOverflowMenuOpen.value, onDismissRequest = onDismissOverflowMenu, - ) { - 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", - ) - } - } + content = { overflowMenu() }, + ) }, ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt index 69ac12e..c098b3f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt @@ -64,6 +64,7 @@ data class NetworkPageUio( val enableCancel: Boolean, ) { companion object { + @Stable fun empty( player: String = "", host: String = "", @@ -167,11 +168,14 @@ fun NetworkScreen( @Composable fun NetworkPage( + modifier: Modifier = Modifier, viewModel: NetworkViewModel = koinViewModel(), ) { val snack = LocalSnackHost.current - Surface { + Surface( + modifier = modifier, + ) { BlurContent( modifier = Modifier.fillMaxSize(), controller = viewModel.blurController, @@ -267,6 +271,7 @@ private fun NetworkContent( } TextButton( + modifier = Modifier.align(alignment = Alignment.End), enabled = player.value.enableActions, onClick = onConnect, ) { @@ -274,6 +279,7 @@ private fun NetworkContent( } TextButton( + modifier = Modifier.align(alignment = Alignment.End), enabled = player.value.enableCancel, onClick = onDisconnect, ) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt index 78418fb..dd2b7a7 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt @@ -64,11 +64,11 @@ class NetworkViewModel( } fun onPortChange(port: String) { - this.portFlow.value = port.toIntOrNull() ?: settings.port + portFlow.value = port.toIntOrNull() ?: settings.port } fun onHostChange(host: String) { - this.hostFlow.value = host + hostFlow.value = host } fun connect() { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt index 2287c75..7c6fdfa 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt @@ -196,6 +196,16 @@ class RollViewModel( rollValue = roll, rollSuccessLimit = rollStep?.success?.last, 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( payload = payload, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsScreen.kt new file mode 100644 index 0000000..abfd139 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsScreen.kt @@ -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), + ) { + + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt index 8337488..73ca61e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt @@ -6,8 +6,10 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.compositionLocalOf 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.typography.LwaTypography +import com.pixelized.desktop.lwa.ui.theme.typography.lwaTypography val LocalLwaTheme = compositionLocalOf { error("Local Snack Controller is not yet ready") @@ -20,17 +22,21 @@ val MaterialTheme.lwa: LwaTheme @Stable data class LwaTheme( - val colorScheme: LwaColorTheme, + val colorScheme: LwaColors, + val typography: LwaTypography, ) @Composable fun LwaTheme( content: @Composable () -> Unit, ) { - val lwaColorTheme = darkLwaColorTheme() + val lwaColors = darkLwaColorTheme() + val lwaTypography = lwaTypography(colors = lwaColors) + val theme = remember { LwaTheme( - colorScheme = lwaColorTheme, + colorScheme = lwaColors, + typography = lwaTypography, ) } @@ -38,7 +44,7 @@ fun LwaTheme( LocalLwaTheme provides theme ) { MaterialTheme( - colors = lwaColorTheme.base, + colors = lwaColors.base, typography = MaterialTheme.typography, shapes = MaterialTheme.shapes, content = content, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColorPalette.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColorPalette.kt index 06a24fc..c9f0d7f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColorPalette.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColorPalette.kt @@ -4,4 +4,10 @@ import androidx.compose.ui.graphics.Color object LwaColorPalette { 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) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColorTheme.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColors.kt similarity index 64% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColorTheme.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColors.kt index 22a4a39..73f4fd0 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColorTheme.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColors.kt @@ -13,15 +13,29 @@ import androidx.compose.ui.unit.dp import kotlin.math.ln @Stable -data class LwaColorTheme( +data class LwaColors( val base: Colors, val elevated: Elevated, val portraitBackgroundBrush: Brush, + val chat: Chat, ) { @Stable data class Elevated( val base1dp: 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 fun darkLwaColorTheme( base: Colors = darkColors(), - elevated: LwaColorTheme.Elevated = LwaColorTheme.Elevated( + elevated: LwaColors.Elevated = LwaColors.Elevated( base1dp = base.calculateElevatedColor( color = base.surface, onColor = base.onSurface, @@ -40,6 +54,16 @@ fun darkLwaColorTheme( onColor = base.onSurface, 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( listOf( @@ -49,11 +73,21 @@ fun darkLwaColorTheme( elevated.base1dp.copy(alpha = 0.5f), 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, elevated = elevated, portraitBackgroundBrush = portraitBackgroundBrush, + chat = chat, ) @ReadOnlyComposable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt new file mode 100644 index 0000000..affabe4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt @@ -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, + ), + ) + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt index 0b56471..34a44d8 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt @@ -10,4 +10,13 @@ data class RollMessage( val rollDifficulty: String? = null, val rollValue: Int, val rollSuccessLimit: Int? = null, -) : MessagePayload \ No newline at end of file + val critical: Critical?, +) : MessagePayload { + enum class Critical { + CRITICAL_SUCCESS, + SPECIAL_SUCCESS, + SUCCESS, + FAILURE, + CRITICAL_FAILURE + } +} \ No newline at end of file