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__roll_history_action">Historique des lancers</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__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.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<SnackbarHostState> {
error("Local Snack Controller is not yet ready")
}
val LocalApplicationScope = compositionLocalOf<ApplicationScope> {
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<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
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(

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.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

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.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<NavHostController> {
error("MainNavHost controller is not yet ready")
@ -18,6 +22,9 @@ val LocalScreenController = compositionLocalOf<NavHostController> {
@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()
}
}
}

View file

@ -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,
)
}
}

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.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<CampaignLayoutScope> {
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<DpSize>,
val rightOverlay: State<DpSize>,
val chatOverlay: State<DpSize>,
)
private fun IntSize.toDp(density: Density) = with(density) {

View file

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

View file

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

View file

@ -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<String>,
networkStatus: State<NetworkRepository.Status>,
isNetworkMenuOpen: State<Boolean>,
isOverflowMenuOpen: State<Boolean>,
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() },
)
},
)
}

View file

@ -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,
) {

View file

@ -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() {

View file

@ -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,

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.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<LwaTheme> {
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,

View file

@ -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)
}

View file

@ -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

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 rollValue: Int,
val rollSuccessLimit: Int? = null,
) : MessagePayload
val critical: Critical?,
) : MessagePayload {
enum class Critical {
CRITICAL_SUCCESS,
SPECIAL_SUCCESS,
SUCCESS,
FAILURE,
CRITICAL_FAILURE
}
}