Change the access to some old screen from the new UI.

This commit is contained in:
Thomas Andres Gomez 2025-02-28 13:31:01 +01:00
parent 1bf0cb7c0a
commit 3941c5dd76
25 changed files with 657 additions and 450 deletions

View file

@ -42,6 +42,8 @@ kotlin {
implementation(libs.ktor.client.negotiation)
// shell
implementation(libs.turtle)
// log
implementation(libs.logback)
}
commonTest.dependencies {

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="m370,880 l-16,-128q-13,-5 -24.5,-12T307,725l-119,50L78,585l103,-78q-1,-7 -1,-13.5v-27q0,-6.5 1,-13.5L78,375l110,-190 119,50q11,-8 23,-15t24,-12l16,-128h220l16,128q13,5 24.5,12t22.5,15l119,-50 110,190 -103,78q1,7 1,13.5v27q0,6.5 -2,13.5l103,78 -110,190 -118,-50q-11,8 -23,15t-24,12L590,880L370,880ZM440,800h79l14,-106q31,-8 57.5,-23.5T639,633l99,41 39,-68 -86,-65q5,-14 7,-29.5t2,-31.5q0,-16 -2,-31.5t-7,-29.5l86,-65 -39,-68 -99,42q-22,-23 -48.5,-38.5T533,266l-13,-106h-79l-14,106q-31,8 -57.5,23.5T321,327l-99,-41 -39,68 86,64q-5,15 -7,30t-2,32q0,16 2,31t7,30l-86,65 39,68 99,-42q22,23 48.5,38.5T427,694l13,106ZM482,620q58,0 99,-41t41,-99q0,-58 -41,-99t-99,-41q-59,0 -99.5,41T342,480q0,58 40.5,99t99.5,41ZM480,480Z"
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="M120,720q-33,0 -56.5,-23.5T40,640q0,-33 23.5,-56.5T120,560h10.5q4.5,0 9.5,2l182,-182q-2,-5 -2,-9.5L320,360q0,-33 23.5,-56.5T400,280q33,0 56.5,23.5T480,360q0,2 -2,20l102,102q5,-2 9.5,-2h21q4.5,0 9.5,2l142,-142q-2,-5 -2,-9.5L760,320q0,-33 23.5,-56.5T840,240q33,0 56.5,23.5T920,320q0,33 -23.5,56.5T840,400h-10.5q-4.5,0 -9.5,-2L678,540q2,5 2,9.5v10.5q0,33 -23.5,56.5T600,640q-33,0 -56.5,-23.5T520,560v-10.5q0,-4.5 2,-9.5L420,438q-5,2 -9.5,2L400,440q-2,0 -20,-2L198,620q2,5 2,9.5v10.5q0,33 -23.5,56.5T120,720Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -7,10 +7,9 @@
<string name="main_page__create_action">Créer une feuille de personnage</string>
<string name="main_page__open_save_directory">Ouvrir le dossier de sauvegarde</string>
<string name="main_page__roll_history_action">Consulter l'historique des lancers</string>
<string name="main_page__roll_history_action">Historique des lancers</string>
<string name="main_page__network_action">Configuration de la table</string>
<string name="roll_page__roll__label">Jet de :</string>
<string name="roll_page__roll__success_label">Réussite si lancer inférieur ou égal à : %1$s</string>
<string name="roll_page__critical_success">Réussite critique</string>

View file

@ -26,9 +26,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.window.ApplicationScope
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler
@ -40,12 +42,15 @@ 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
import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetWindow
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.characterSheet.CharacterSheetMainNavHost
import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
import com.pixelized.desktop.lwa.ui.theme.LwaTheme
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -88,74 +93,82 @@ fun ApplicationScope.App() {
) {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(size = DpSize(width = 800.dp, height = maxWindowHeight)),
title = runBlocking { getString(Res.string.app_name) },
onKeyEvent = { event ->
keyEventHandlers.reversed().any { it(event) }
},
) {
LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
Scaffold(
snackbarHost = {
Column(
modifier = Modifier.padding(all = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp)
) {
SnackbarHost(
hostState = snackHostState,
snackbar = {
Snackbar(
snackbarData = it,
)
}
)
SnackbarHost(
hostState = errorSnackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError,
actionColor = MaterialTheme.colors.onError,
)
}
)
}
},
content = {
MainScreen()
}
)
NetworkSnackHandler(
snack = snackHostState,
)
WindowsHandler(
windowController = windowController,
)
}
}
MainWindowScreen()
}
}
}
@Composable
private fun MainScreen(
private fun MainWindowScreen(
campaignViewModel: CampaignViewModel = koinViewModel(),
networkViewModel: NetworkViewModel = koinViewModel(),
rollViewModel: RollHistoryViewModel = koinViewModel(),
) {
LaunchedEffect(Unit) {
networkViewModel.connect()
campaignViewModel.init()
}
MainNavHost()
val snackHostState = LocalSnackHost.current
val errorSnackHostState = LocalErrorSnackHost.current
val windowController = LocalWindowController.current
LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
Scaffold(
snackbarHost = {
Column(
modifier = Modifier.padding(all = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp)
) {
SnackbarHost(
hostState = snackHostState,
snackbar = {
Snackbar(
snackbarData = it,
)
}
)
SnackbarHost(
hostState = errorSnackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError,
actionColor = MaterialTheme.colors.onError,
)
}
)
}
},
content = {
MainNavHost()
}
)
NetworkSnackHandler(
snack = snackHostState,
)
WindowsHandler(
windowController = windowController,
rollViewModel = rollViewModel,
)
}
}
}
@Composable
private fun WindowsHandler(
windowController: WindowController,
rollViewModel: RollHistoryViewModel = koinViewModel(),
) {
WindowsNavHost(
controller = windowController,
@ -173,7 +186,11 @@ private fun WindowsHandler(
),
)
is RollHistoryWindow -> RollHistoryPage()
is RollHistoryWindow -> RollHistoryPage(
viewModel = rollViewModel,
)
is NetworkWindows -> NetworkPage()
}
}
)

View file

@ -34,7 +34,6 @@ import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.websocket.WebSockets
@ -56,12 +55,9 @@ val appModuleDependencies
val toolsDependencies
get() = module {
single<HttpClientEngine> {
OkHttp.create()
}
single {
HttpClient(
engine = get()
engine = OkHttp.create(),
) {
install(WebSockets) {
pingIntervalMillis = 20_000

View file

@ -7,6 +7,10 @@ import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.serialization.kotlinx.json.json
import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -22,10 +26,11 @@ import kotlinx.serialization.json.Json
class NetworkRepository(
private val settingsRepository: SettingsRepository,
private val client: HttpClient,
private val json: Json,
) {
private val scope = CoroutineScope(Dispatchers.IO)
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
private var networkJob: Job? = null
private var client: HttpClient? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
private val incomingMessageBuffer = MutableSharedFlow<Message>()
@ -41,10 +46,11 @@ class NetworkRepository(
onFailure: (Exception) -> Unit = { },
onClose: () -> Unit = { },
) {
client = buildNewClient()
networkJob?.cancel()
networkJob = scope.launch {
networkJob = scope.launch(Dispatchers.IO) {
try {
client.connectWebSocket(host = host, port = port) {
client?.connectWebSocket(host = host, port = port) {
_status.value = Status.CONNECTED
onConnect()
@ -78,7 +84,8 @@ class NetworkRepository(
fun disconnect() {
networkJob?.cancel()
scope.launch {
client.close()
client?.close()
client = null
}
}
@ -96,6 +103,17 @@ class NetworkRepository(
}
}
private fun buildNewClient() = HttpClient(
engine = OkHttp.create(),
) {
install(WebSockets) {
pingIntervalMillis = 20_000
}
install(ContentNegotiation) {
json(json)
}
}
enum class Status {
CONNECTED,
DISCONNECTED

View file

@ -13,7 +13,7 @@ suspend fun HttpClient.connectWebSocket(
) {
webSocket(
method = HttpMethod.Get,
host = host,
host = host.removePrefix("http://"),
port = port,
path = "/ws",
block = block,

View file

@ -1,15 +1,14 @@
package com.pixelized.desktop.lwa.repository.settings
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJsonV1
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
class SettingsFactory(
private val useCase: SettingsUseCase,
) {
fun convertToJson(
settings: Settings,
): SettingsJson {

View file

@ -5,5 +5,5 @@ data class Settings(
val port: Int,
val playerName: String,
) {
val root: String get() = "http://${host}:${port}"
val root: String get() = "http://${"${host}:${port}".removePrefix("http://")}"
}

View file

@ -3,7 +3,7 @@ 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.network.NetworkPage
import com.pixelized.desktop.lwa.ui.screen.network.NetworkScreen
object NetworkDestination {
private const val ROUTE = "network"
@ -16,7 +16,7 @@ fun NavGraphBuilder.composableNetworkPage() {
composable(
route = NetworkDestination.baseRoute(),
) {
NetworkPage()
NetworkScreen()
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.desktop.lwa.ui.navigation.window.destination
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.navigation.window.WindowController
@Stable
class NetworkWindows(
title: String,
size: DpSize,
) : Window(
title = title,
size = size,
)
fun WindowController.navigateToNetwork(
title: String = "",
) {
showWindow(
window = NetworkWindows(
title = title, size = DpSize(
width = 464.dp,
height = 300.dp,
)
)
)
}

View file

@ -13,12 +13,9 @@ 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.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -34,33 +31,24 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController
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.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToOldMainPage
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory
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.Toolbar
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.roll.RollPage
import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
import lwacharactersheet.composeapp.generated.resources.ic_table_24dp
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun MainPage(
campaignViewModel: CampaignViewModel = koinViewModel(),
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
@ -76,9 +64,7 @@ fun MainPage(
else -> false
}
}
val windows = LocalWindowController.current
val screen = LocalScreenController.current
val scope = rememberCoroutineScope()
val blurController = rememberBlurContentController()
@ -92,31 +78,7 @@ fun MainPage(
CampaignScreenLayout(
modifier = Modifier.fillMaxSize(),
top = {
Toolbar(
title = campaignViewModel.title.collectAsState(initial = "").value,
actions = {
IconButton(
onClick = {
windows.navigateToRollHistory()
},
) {
Icon(
painter = painterResource(Res.drawable.ic_d20_24dp),
contentDescription = null,
)
}
IconButton(
onClick = {
screen.navigateToOldMainPage()
},
) {
Icon(
painter = painterResource(Res.drawable.ic_table_24dp),
contentDescription = null,
)
}
},
)
CampaignToolbar()
},
bottom = {
Surface(

View file

@ -5,8 +5,10 @@ import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -14,6 +16,7 @@ class CampaignViewModel(
private val characterRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
private val campaignRepository: CampaignRepository,
private val network: NetworkRepository,
) : ViewModel() {
val title: Flow<String> = campaignRepository.campaignFlow
@ -21,15 +24,22 @@ class CampaignViewModel(
fun init() {
viewModelScope.launch {
campaignRepository.campaignFlow.collectLatest {
it.characters.keys.forEach { id ->
characterRepository.characterDetail(
characterSheetId = id.characterSheetId,
forceUpdate = true,
)
alterationRepository.updateActiveAlterations(
characterInstanceId = id,
)
combine(
network.status,
campaignRepository.campaignFlow,
) { status, campaign ->
status to campaign
}.collectLatest { (status, campaign) ->
if (status == NetworkRepository.Status.CONNECTED) {
campaign.characters.keys.forEach { id ->
characterRepository.characterDetail(
characterSheetId = id.characterSheetId,
forceUpdate = true,
)
alterationRepository.updateActiveAlterations(
characterInstanceId = id,
)
}
}
}
}

View file

@ -0,0 +1,145 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
import androidx.compose.foundation.layout.padding
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
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.dp
import com.pixelized.desktop.lwa.LocalWindowController
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.window.destination.navigateToRollHistory
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
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.main_page__roll_history_action
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun CampaignToolbar(
campaignViewModel: CampaignViewModel = koinViewModel(),
) {
val windows = LocalWindowController.current
val screen = LocalScreenController.current
val isOverflowMenuOpen = remember { mutableStateOf(false) }
CampaignToolbarContent(
title = campaignViewModel.title.collectAsState(initial = ""),
isOverflowMenuOpen = isOverflowMenuOpen,
onOverflow = {
isOverflowMenuOpen.value = isOverflowMenuOpen.value.not()
},
onDismissOverflowMenu = {
isOverflowMenuOpen.value = false
},
onRollHistory = {
windows.navigateToRollHistory()
},
onNetwork = {
windows.navigateToNetwork()
},
onOlUi = {
screen.navigateToOldMainPage()
},
)
}
@Composable
private fun CampaignToolbarContent(
modifier: Modifier = Modifier,
title: State<String>,
isOverflowMenuOpen: State<Boolean>,
onOverflow: () -> Unit,
onDismissOverflowMenu: () -> Unit,
onRollHistory: () -> Unit,
onNetwork: () -> Unit,
onOlUi: () -> Unit,
) {
TopAppBar(
modifier = modifier,
title = {
Text(
text = title.value,
)
},
actions = {
IconButton(
onClick = onOverflow,
) {
Icon(
imageVector = Icons.Default.MoreVert,
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
DropdownMenu(
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",
)
}
}
},
)
}

View file

@ -1,52 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
@Composable
fun Toolbar(
modifier: Modifier = Modifier,
title: String,
actions: @Composable RowScope.() -> Unit = { },
) {
Surface(
modifier = modifier,
elevation = 1.dp,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.minimumInteractiveComponentSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.h6,
text = title,
)
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.lwa.colorScheme.base.primary,
) {
Row(
modifier = Modifier.padding(end = 8.dp),
content = actions,
)
}
}
}
}

View file

@ -124,7 +124,7 @@ data class CharacterSheetPageUio(
data class Roll(
val label: String,
val value: String,
val roll: RollActionUio?,
val roll: RollActionUio,
)
}
@ -187,7 +187,7 @@ fun CharacterSheetPage(
onUseSkill = viewModel::onUseSkill,
onRoll = { roll ->
blurController.show()
rollViewModel.prepareRoll(sheet = sheet, roll = roll)
rollViewModel.prepareRoll(roll.roll)
viewModel.showRollOverlay()
},
)

View file

@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
@ -32,7 +34,6 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToNetw
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheet
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.Toolbar
import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res
@ -117,168 +118,177 @@ fun MainPageContent(
onNetwork: () -> Unit,
onMainPage: () -> Unit,
) {
Column(
Scaffold(
modifier = modifier,
) {
Toolbar(
title = runBlocking { getString(Res.string.app_name) },
actions = {
TextButton(
onClick = onMainPage,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
topBar = {
TopAppBar(
title = {
Text(
text = runBlocking { getString(Res.string.app_name) },
)
},
actions = {
TextButton(
onClick = onMainPage,
) {
Icon(
painter = painterResource(Res.drawable.ic_swords_24dp),
contentDescription = null,
)
Text(
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = "Nouvelle interface utilisateur",
)
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_swords_24dp),
contentDescription = null,
)
Text(
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = "Nouvelle interface utilisateur",
)
}
}
}
}
)
Box(
modifier = Modifier
.verticalScroll(state = scrollState)
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
)
},
content = {
Column {
if (characters.value.isNotEmpty()) {
Box(
modifier = Modifier
.verticalScroll(state = scrollState)
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
Column {
characters.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) },
if (characters.value.isNotEmpty()) {
Column {
characters.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) },
) {
Text(
modifier = Modifier.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = sheet.name,
)
}
}
}
}
if (characters.value.isNotEmpty() && npcs.value.isNotEmpty()) {
Spacer(modifier = Modifier.height(height = 24.dp))
}
if (npcs.value.isNotEmpty()) {
Column {
npcs.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) },
) {
Text(
modifier = Modifier.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = sheet.name,
)
}
}
}
}
Spacer(modifier = Modifier.height(height = 24.dp))
TextButton(
onClick = onCreateCharacter,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_file_24dp),
contentDescription = null,
)
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = sheet.name,
text = stringResource(Res.string.main_page__create_action),
)
}
}
}
}
if (characters.value.isNotEmpty() && npcs.value.isNotEmpty()) {
Spacer(modifier = Modifier.height(height = 24.dp))
}
if (npcs.value.isNotEmpty()) {
Column {
npcs.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) },
TextButton(
onClick = onOpenSaveDirectory,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_folder_24dp),
contentDescription = null,
)
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = sheet.name,
text = stringResource(Res.string.main_page__open_save_directory),
)
}
}
}
}
Spacer(modifier = Modifier.height(height = 24.dp))
TextButton(
enabled = enableRollHistory.value,
onClick = onRollHistory,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_d20_24dp),
contentDescription = null,
)
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__roll_history_action),
)
}
}
TextButton(
onClick = onCreateCharacter,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_file_24dp),
contentDescription = null,
)
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__create_action),
)
}
}
TextButton(
onClick = onOpenSaveDirectory,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_folder_24dp),
contentDescription = null,
)
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__open_save_directory),
)
}
}
TextButton(
enabled = enableRollHistory.value,
onClick = onRollHistory,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_d20_24dp),
contentDescription = null,
)
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__roll_history_action),
)
}
}
TextButton(
onClick = onNetwork,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_table_24dp),
contentDescription = null,
)
Text(
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = stringResource(Res.string.main_page__network_action),
)
TextButton(
onClick = onNetwork,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_table_24dp),
contentDescription = null,
)
Text(
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = stringResource(Res.string.main_page__network_action),
)
}
}
}
}
}
}
}
},
)
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -82,7 +83,7 @@ data class NetworkPageUio(
}
@Composable
fun NetworkPage(
fun NetworkScreen(
viewModel: NetworkViewModel = koinViewModel(),
) {
val screen = LocalScreenController.current
@ -97,17 +98,43 @@ fun NetworkPage(
) {
BlurContent(
modifier = Modifier.fillMaxSize(),
controller = viewModel.controller,
controller = viewModel.blurController,
) {
NetworkContent(
Scaffold(
modifier = Modifier.fillMaxSize(),
player = viewModel.network.collectAsState(),
onBack = { screen.popBackStack() },
onPlayerChange = viewModel::onPlayerNameChange,
onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange,
onConnect = viewModel::connect,
onDisconnect = viewModel::disconnect,
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.network__title),
)
},
navigationIcon = {
IconButton(
onClick = { screen.popBackStack() },
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->
NetworkContent(
modifier = Modifier.fillMaxSize(),
paddingValues = paddingValues,
player = viewModel.network.collectAsState(),
onPlayerChange = viewModel::onPlayerNameChange,
onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange,
onConnect = viewModel::connect,
onDisconnect = viewModel::disconnect,
)
}
)
}
@ -138,96 +165,119 @@ fun NetworkPage(
}
}
@Composable
fun NetworkPage(
viewModel: NetworkViewModel = koinViewModel(),
) {
val snack = LocalSnackHost.current
Surface {
BlurContent(
modifier = Modifier.fillMaxSize(),
controller = viewModel.blurController,
) {
NetworkContent(
modifier = Modifier.fillMaxSize(),
player = viewModel.network.collectAsState(),
onPlayerChange = viewModel::onPlayerNameChange,
onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange,
onConnect = viewModel::connect,
onDisconnect = viewModel::disconnect,
)
}
AnimatedContent(
modifier = Modifier.size(size = 64.dp),
targetState = viewModel.isLoading.value,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) {
when (it) {
true -> CircularProgressIndicator()
else -> Box(modifier = Modifier)
}
}
LaunchedEffect(Unit) {
viewModel.message.collect {
snack.showSnackbar(
message = it,
duration = SnackbarDuration.Short,
)
}
}
ErrorSnack(
error = viewModel.networkError,
)
}
}
@Composable
private fun NetworkContent(
modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(),
paddingValues: PaddingValues = PaddingValues(),
player: State<NetworkPageUio>,
onBack: () -> Unit,
onPlayerChange: (String) -> Unit,
onHostChange: (String) -> Unit,
onPortChange: (String) -> Unit,
onConnect: () -> Unit,
onDisconnect: () -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.network__title),
)
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(paddingValues)
.padding(all = 16.dp)
.then(other = modifier),
) {
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__player_name__label)) },
onValueChange = { onPlayerChange(it) },
value = player.value.player,
)
Spacer(
modifier = Modifier.height(16.dp),
)
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
TextField(
modifier = Modifier.weight(1f),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__host__label)) },
onValueChange = { onHostChange(it) },
value = player.value.host,
)
TextField(
modifier = Modifier.width(100.dp),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__port__label)) },
onValueChange = { onPortChange(it) },
value = player.value.port,
)
},
content = { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(paddingValues)
.padding(all = 16.dp),
) {
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__player_name__label)) },
onValueChange = { onPlayerChange(it) },
value = player.value.player,
)
Spacer(
modifier = Modifier.height(16.dp),
)
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
TextField(
modifier = Modifier.weight(1f),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__host__label)) },
onValueChange = { onHostChange(it) },
value = player.value.host,
)
TextField(
modifier = Modifier.width(100.dp),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__port__label)) },
onValueChange = { onPortChange(it) },
value = player.value.port,
)
}
TextButton(
enabled = player.value.enableActions,
onClick = onConnect,
) {
Text(text = stringResource(Res.string.network__socket__connect_action))
}
TextButton(
enabled = player.value.enableCancel,
onClick = onDisconnect,
) {
Text(text = stringResource(Res.string.network__socket__disconnect_action))
}
}
}
)
TextButton(
enabled = player.value.enableActions,
onClick = onConnect,
) {
Text(text = stringResource(Res.string.network__socket__connect_action))
}
TextButton(
enabled = player.value.enableCancel,
onClick = onDisconnect,
) {
Text(text = stringResource(Res.string.network__socket__disconnect_action))
}
}
}

View file

@ -9,6 +9,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -22,8 +23,8 @@ class NetworkViewModel(
private val factory: NetworkFactory,
) : ViewModel() {
private val settings = settingsRepository.settings()
private val host = mutableStateOf(settings.host)
private val port = mutableStateOf(settings.port)
private val hostFlow = MutableStateFlow(settings.host)
private val portFlow = MutableStateFlow(settings.port)
private val _networkError = MutableSharedFlow<ErrorSnackUio>()
val networkError: SharedFlow<ErrorSnackUio> get() = _networkError
@ -34,17 +35,19 @@ class NetworkViewModel(
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
val controller: BlurContentController = BlurContentController()
val blurController: BlurContentController = BlurContentController()
val network: StateFlow<NetworkPageUio> = combine(
settingsRepository.settingsFlow(),
networkRepository.status,
) { settings, status ->
hostFlow,
portFlow,
) { settings, status, host, port ->
factory.convertToUio(
player = settings.playerName,
status = status,
host = host.value,
port = port.value,
host = host,
port = port,
)
}.stateIn(
scope = viewModelScope,
@ -61,36 +64,36 @@ class NetworkViewModel(
}
fun onPortChange(port: String) {
this.port.value = port.toIntOrNull() ?: settings.port
this.portFlow.value = port.toIntOrNull() ?: settings.port
}
fun onHostChange(host: String) {
this.host.value = host
this.hostFlow.value = host
}
fun connect() {
controller.show()
blurController.show()
_isLoading.value = true
if (settings.host != host.value || settings.port != port.value) {
if (settings.host != hostFlow.value || settings.port != portFlow.value) {
settingsRepository.update(
settings = settings.copy(
host = host.value,
port = port.value
host = hostFlow.value,
port = portFlow.value
)
)
}
networkRepository.connect(
host = host.value,
port = port.value,
host = hostFlow.value,
port = portFlow.value,
onConnect = {
_isLoading.value = false
controller.hide()
blurController.hide()
},
onFailure = {
_isLoading.value = false
controller.hide()
blurController.hide()
viewModelScope.launch {
_networkError.emit(ErrorSnackUio(it))
}

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
@ -33,6 +34,7 @@ import org.jetbrains.compose.resources.getString
class RollViewModel(
private val characterSheetRepository: CharacterSheetRepository,
private val settingsRepository: SettingsRepository,
private val skillComputation: ExpressionUseCase,
private val skillStepUseCase: SkillStepUseCase,
private val networkRepository: NetworkRepository,

View file

@ -42,7 +42,7 @@ fun RollHistoryItem(
modifier = Modifier.weight(1f)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),

View file

@ -13,6 +13,7 @@ import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -26,12 +27,13 @@ import org.koin.compose.viewmodel.koinViewModel
fun RollHistoryPage(
viewModel: RollHistoryViewModel = koinViewModel(),
) {
val rolls = viewModel.rolls.collectAsState()
Surface(
modifier = Modifier.fillMaxSize(),
) {
RollHistoryContent(
modifier = Modifier.fillMaxSize(),
rolls = viewModel.rolls,
rolls = rolls,
)
}
}

View file

@ -1,47 +1,44 @@
package com.pixelized.desktop.lwa.ui.screen.rollhistory
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.stateIn
class RollHistoryViewModel(
private val characterRepository: CharacterSheetRepository,
private val rollRepository: RollHistoryRepository,
characterRepository: CharacterSheetRepository,
rollRepository: RollHistoryRepository,
) : ViewModel() {
private val _rolls = mutableStateOf((emptyList<RollHistoryItemUio>()))
val rolls: State<List<RollHistoryItemUio>> get() = _rolls
init {
viewModelScope.launch {
combine(
characterRepository.characterSheetPreviewFlow,
rollRepository.rolls,
) { sheets: List<CharacterSheetPreview>, content ->
_rolls.value.toMutableList().apply {
val name = sheets.firstOrNull { it.id == content.characterId }?.name ?: ""
add(
index = 0,
element = RollHistoryItemUio(
character = name,
skillLabel = content.skillLabel,
rollDifficulty = content.rollDifficulty,
resultLabel = content.resultLabel,
rollValue = content.rollValue,
rollSuccessLimit = content.rollSuccessLimit,
)
)
}
}.collect { content ->
_rolls.value = content
}
private var _rolls: List<RollHistoryItemUio> = emptyList()
val rolls = combine(
characterRepository.characterSheetPreviewFlow,
rollRepository.rolls,
) { sheets, message ->
val name = sheets.firstOrNull { it.id == message.characterId }?.name ?: ""
val roll = RollHistoryItemUio(
character = name,
skillLabel = message.skillLabel,
rollDifficulty = message.rollDifficulty,
resultLabel = message.resultLabel,
rollValue = message.rollValue,
rollSuccessLimit = message.rollSuccessLimit,
)
_rolls = _rolls.toMutableList().also {
if (it.size == MAX_ROLL_HISTORY_SIZE) it.removeAt(index = it.lastIndex)
it.add(index = 0, element = roll)
}
_rolls
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = _rolls,
)
companion object {
const val MAX_ROLL_HISTORY_SIZE = 100
}
}

View file

@ -7,8 +7,8 @@ androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10"
ktor = "3.0.1"
koin = "4.0.0"
turtle = "0.5.0"
logback = "1.5.11"
turtle = "0.10.0"
logback = "1.5.17"
coil = "3.1.0"
[plugins]
@ -47,6 +47,7 @@ ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref =
ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor" }
ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" }
ktor-server-negotiation = { group = 'io.ktor', name = 'ktor-server-content-negotiation', version.ref = "ktor" }
# Utilitary run command line.
turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" }
# Utilitary logging use by Ktor.
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }