Add roll sharing feature over the WebSocket.

This commit is contained in:
Thomas Andres Gomez 2024-11-09 22:57:26 +01:00
parent 0e5fee6771
commit f92922c228
17 changed files with 476 additions and 183 deletions

View file

@ -2,6 +2,7 @@
<resources>
<string name="main_page__create_action">Créer une feuille de personnage</string>
<string name="main_page__network_action">Configuration réseau</string>
<string name="main_page__roll_history_action">Consulter l'historique des lancés</string>
<string name="roll_page__critical_success">Réussite critique</string>
<string name="roll_page__special_success">Réussite spéciale</string>
@ -81,4 +82,6 @@
<string name="network__socket__type_server">Serveur</string>
<string name="network__socket__type_client">Client</string>
<string name="network__socket__type_none">Aucun</string>
<string name="roll_history__title">Historique des lancés</string>
</resources>

View file

@ -1,11 +1,16 @@
package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -13,9 +18,19 @@ import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.navigation.MainNavHost
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetMainNavHost
import com.pixelized.desktop.lwa.screen.main.CharacterUio
import com.pixelized.desktop.lwa.theme.LwaTheme
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__title
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
val LocalSnackHost = compositionLocalOf<SnackbarHostState> {
error("Local Snack Controller is not yet ready")
}
val LocalWindowController = compositionLocalOf<WindowController> {
error("Local Window Controller is not yet ready")
}
@ -24,23 +39,47 @@ val LocalWindowController = compositionLocalOf<WindowController> {
data class WindowController(
private val onCloseRequest: () -> Unit
) {
fun close() = onCloseRequest()
val sheet: State<Set<CharacterUio>> get() = _sheet
val create: State<Set<Int>> get() = _create
fun showCreateCharacterSheet() {
_create.value = _create.value.toMutableSet().apply { add(size) }
}
fun hideCreateCharacterSheet(id: Int) {
_create.value = _create.value.toMutableSet().apply { remove(id) }
}
fun showCharacterSheet(sheet: CharacterUio) {
_sheet.value = _sheet.value.toMutableSet().apply { add(sheet) }
}
fun hideCharacterSheet(sheet: CharacterUio) {
_sheet.value = _sheet.value.toMutableSet().apply { remove(sheet) }
}
fun closeWindows() = onCloseRequest()
companion object {
private val _sheet = mutableStateOf<Set<CharacterUio>>(emptySet())
private val _create = mutableStateOf<Set<Int>>(emptySet())
}
}
@Composable
@Preview
fun ApplicationScope.App() {
val controller = remember {
WindowController(
onCloseRequest = ::exitApplication
)
}
val controller = remember { WindowController(onCloseRequest = ::exitApplication) }
val snackHostState = remember { SnackbarHostState() }
CompositionLocalProvider(
LocalWindowController provides controller,
LocalSnackHost provides snackHostState,
) {
Window(
onCloseRequest = {
controller.close()
controller.closeWindows()
},
state = rememberWindowState(
width = 320.dp + 64.dp,
@ -52,9 +91,86 @@ fun ApplicationScope.App() {
Surface(
modifier = Modifier.fillMaxSize()
) {
MainNavHost()
Scaffold(
snackbarHost = {
SnackbarHost(
hostState = snackHostState,
)
},
content = {
MainNavHost()
}
)
HandleCharacterSheet(
sheets = controller.sheet,
onCloseRequest = { controller.hideCharacterSheet(sheet = it) }
)
HandleCharacterSheetCreation(
sheets = controller.create,
onCloseRequest = { controller.hideCreateCharacterSheet(id = it) },
)
}
}
}
}
}
@Composable
fun HandleCharacterSheet(
sheets: State<Set<CharacterUio>>,
onCloseRequest: (id: CharacterUio) -> Unit,
) {
sheets.value.forEach { sheet ->
val controller = remember {
WindowController(
onCloseRequest = { onCloseRequest(sheet) }
)
}
CompositionLocalProvider(
LocalWindowController provides controller,
) {
Window(
onCloseRequest = { onCloseRequest(sheet) },
state = rememberWindowState(
width = 400.dp + 64.dp,
height = 900.dp,
),
title = sheet.name,
) {
CharacterSheetMainNavHost(
startDestination = CharacterSheetDestination.navigationRoute(id = sheet.id)
)
}
}
}
}
@Composable
fun HandleCharacterSheetCreation(
sheets: State<Set<Int>>,
onCloseRequest: (id: Int) -> Unit,
) {
sheets.value.forEach { sheet ->
val controller = remember {
WindowController(
onCloseRequest = { onCloseRequest(sheet) }
)
}
CompositionLocalProvider(
LocalWindowController provides controller,
) {
Window(
onCloseRequest = { controller.closeWindows() },
state = rememberWindowState(
width = 400.dp + 64.dp,
height = 900.dp,
),
title = stringResource(Res.string.character_sheet_edit__title),
) {
CharacterSheetMainNavHost(
startDestination = CharacterSheetEditDestination.navigationRoute(id = null)
)
}
}
}
}

View file

@ -9,6 +9,8 @@ import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.navigation.destination.MainDestination
import com.pixelized.desktop.lwa.navigation.destination.composableMainPage
import com.pixelized.desktop.lwa.navigation.destination.composableNetworkPage
import com.pixelized.desktop.lwa.navigation.destination.composableRollHistory
import com.pixelized.desktop.lwa.screen.main.MainPageViewModel
val LocalScreenController = compositionLocalOf<NavHostController> {
error("MainNavHost controller is not yet ready")
@ -28,6 +30,7 @@ fun MainNavHost(
) {
composableMainPage()
composableNetworkPage()
composableRollHistory()
}
}
}

View file

@ -4,6 +4,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.screen.main.MainPage
import com.pixelized.desktop.lwa.screen.main.MainPageViewModel
object MainDestination {
private const val ROUTE = "main"

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.screen.rollhistory.RollHistoryPage
object RollHistoryDestination {
private const val ROUTE = "roll_history"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableRollHistory() {
composable(
route = RollHistoryDestination.baseRoute()
) {
RollHistoryPage()
}
}
fun NavHostController.navigateToRollHistory() {
val route = RollHistoryDestination.navigationRoute()
navigate(route = route)
}

View file

@ -4,6 +4,7 @@ import com.pixelized.desktop.lwa.repository.network.helper.client
import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket
import com.pixelized.desktop.lwa.repository.network.helper.server
import com.pixelized.desktop.lwa.repository.network.protocol.Message
import com.pixelized.desktop.lwa.repository.network.protocol.MessageContent
import io.ktor.client.HttpClient
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.netty.NettyApplicationEngine
@ -15,8 +16,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
@ -31,8 +32,9 @@ object NetworkRepository {
private var server: Server? = null
private var client: Client? = null
private val messageResponseFlow = MutableSharedFlow<String>()
private val sharedFlow = messageResponseFlow.asSharedFlow()
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
private val incomingMessageBuffer = MutableSharedFlow<Message>()
val data: SharedFlow<Message> get() = incomingMessageBuffer
private val _player = MutableStateFlow("")
val player: StateFlow<String> get() = _player
@ -57,17 +59,19 @@ object NetworkRepository {
println("Server launched")
val job = launch {
sharedFlow.collect { message ->
println("Broadcast: $message")
send(Frame.Text(message))
// send local message to the clients
outgoingMessageBuffer.collect { message ->
send(Json.encodeToFrame(message = message))
}
}
runCatching {
// watching for clients incoming message
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val receivedText = frame.readText()
messageResponseFlow.emit(receivedText)
val message = Json.decodeFromFrame(frame = frame)
incomingMessageBuffer.emit(message)
// broadcast to clients the message
outgoingMessageBuffer.emit(message)
}
}
}.onFailure { exception ->
@ -108,17 +112,17 @@ object NetworkRepository {
println("Client launched")
val job = launch {
sharedFlow.collect { message ->
println("Send: $message")
send(Frame.Text(message))
// send message to the server
outgoingMessageBuffer.collect { message ->
send(Json.encodeToFrame(message = message))
}
}
runBlocking {
// watching for server incoming message
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val receivedText = frame.readText()
println("client received: $receivedText")
val message = Json.decodeFromFrame(frame = frame)
incomingMessageBuffer.emit(message)
}
}
}.also {
@ -144,15 +148,19 @@ object NetworkRepository {
}
}
fun share(
type: String,
value: String,
suspend fun share(
content: MessageContent,
) {
if (status.value == Status.CONNECTED) {
scope.launch {
val message = Message(from = player.value, type = type, value = value)
val json = Json.encodeToJsonElement(message)
messageResponseFlow.emit(json.toString())
val message = Message(
from = player.value,
value = content,
)
// emit the message into the outgoing buffer
outgoingMessageBuffer.emit(message)
// emit the message into the incoming buffer IF we are the server
if (type.value == Type.SERVER) {
incomingMessageBuffer.emit(message)
}
}
}
@ -167,4 +175,14 @@ object NetworkRepository {
SERVER,
NONE,
}
}
private fun Json.decodeFromFrame(frame: Frame.Text): Message {
val json = frame.readText()
return decodeFromString<Message>(json)
}
private fun Json.encodeToFrame(message: Message): Frame {
val json = encodeToJsonElement(message)
return Frame.Text(text = json.toString())
}

View file

@ -3,8 +3,7 @@ package com.pixelized.desktop.lwa.repository.network.protocol
import kotlinx.serialization.Serializable
@Serializable
class Message(
data class Message(
val from: String,
val type: String,
val value: String,
val value: MessageContent,
)

View file

@ -0,0 +1,6 @@
package com.pixelized.desktop.lwa.repository.network.protocol
import kotlinx.serialization.Serializable
@Serializable
sealed interface MessageContent

View file

@ -0,0 +1,9 @@
package com.pixelized.desktop.lwa.repository.network.protocol
import kotlinx.serialization.Serializable
@Serializable
data class RollMessage(
val label: String,
val roll: Int,
): MessageContent

View file

@ -0,0 +1,41 @@
package com.pixelized.desktop.lwa.repository.roll
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.protocol.Message
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
object RollHistoryRepository {
private val scope = CoroutineScope(Dispatchers.IO)
private val network = NetworkRepository
val rolls: SharedFlow<Message> = network.data
.mapNotNull { it.takeIf { it.value is RollMessage } }
.shareIn(
scope = scope,
started = SharingStarted.Eagerly,
)
init {
scope.launch {
network.data.collect {
println(it)
}
}
}
suspend fun share(
label: String,
roll: Int,
) {
network.share(
content = RollMessage(label = label, roll = roll)
)
}
}

View file

@ -24,7 +24,6 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
@ -124,7 +123,7 @@ fun CharacterSheetPage(
scope.launch {
viewModel.deleteCharacter(id = sheet.id)
if (screen.popBackStack().not()) {
window.close()
window.closeWindows()
}
}
},
@ -191,16 +190,6 @@ fun CharacterSheetPageContent(
)
}
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->

View file

@ -92,7 +92,7 @@ fun CharacterSheetEditPage(
scope.launch {
viewModel.save()
if (screen.popBackStack().not()) {
window.close()
window.closeWindows()
}
}
},

View file

@ -3,7 +3,6 @@ package com.pixelized.desktop.lwa.screen.main
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -27,16 +26,17 @@ import androidx.compose.ui.window.rememberWindowState
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.WindowController
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreenController
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.navigation.destination.navigateToNetwork
import com.pixelized.desktop.lwa.navigation.destination.navigateToRollHistory
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetMainNavHost
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__title
import lwacharactersheet.composeapp.generated.resources.main_page__create_action
import lwacharactersheet.composeapp.generated.resources.main_page__network_action
import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action
import org.jetbrains.compose.resources.stringResource
@Stable
@ -49,6 +49,7 @@ data class CharacterUio(
fun MainPage(
viewModel: MainPageViewModel = viewModel { MainPageViewModel() },
) {
val window = LocalWindowController.current
val screen = LocalScreenController.current
Surface(
@ -64,10 +65,13 @@ fun MainPage(
MainPageContent(
characters = viewModel.characters,
onCharacter = {
viewModel.showCharacterSheet(sheet = it)
window.showCharacterSheet(sheet = it)
},
onCreateCharacter = {
viewModel.showCreateCharacterSheet()
window.showCreateCharacterSheet()
},
onRollHistory = {
screen.navigateToRollHistory()
},
onNetwork = {
screen.navigateToNetwork()
@ -75,140 +79,72 @@ fun MainPage(
)
}
}
HandleCharacterSheet(
sheets = viewModel.sheet,
onCloseRequest = { viewModel.hideCharacterSheet(sheet = it) }
)
HandleCharacterSheetCreation(
sheets = viewModel.create,
onCloseRequest = { viewModel.hideCreateCharacterSheet(id = it) },
)
}
@Composable
fun HandleCharacterSheet(
sheets: State<Set<CharacterUio>>,
onCloseRequest: (id: CharacterUio) -> Unit,
) {
sheets.value.forEach { sheet ->
val controller = remember {
WindowController(
onCloseRequest = { onCloseRequest(sheet) }
)
}
CompositionLocalProvider(
LocalWindowController provides controller,
) {
Window(
onCloseRequest = { onCloseRequest(sheet) },
state = rememberWindowState(
width = 400.dp + 64.dp,
height = 900.dp,
),
title = sheet.name,
) {
CharacterSheetMainNavHost(
startDestination = CharacterSheetDestination.navigationRoute(id = sheet.id)
)
}
}
}
}
@Composable
fun HandleCharacterSheetCreation(
sheets: State<Set<Int>>,
onCloseRequest: (id: Int) -> Unit,
) {
sheets.value.forEach { sheet ->
val controller = remember {
WindowController(
onCloseRequest = { onCloseRequest(sheet) }
)
}
CompositionLocalProvider(
LocalWindowController provides controller,
) {
Window(
onCloseRequest = { controller.close() },
state = rememberWindowState(
width = 400.dp + 64.dp,
height = 900.dp,
),
title = stringResource(Res.string.character_sheet_edit__title),
) {
CharacterSheetMainNavHost(
startDestination = CharacterSheetEditDestination.navigationRoute(id = null)
)
}
}
}
}
@Composable
fun MainPageContent(
modifier: Modifier = Modifier,
characters: State<List<CharacterUio>>,
onCharacter: (CharacterUio) -> Unit,
onCreateCharacter: () -> Unit,
onRollHistory: () -> Unit,
onNetwork: () -> Unit,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Spacer(
modifier = Modifier.weight(weight = 1f)
)
DecoratedBox {
Column(
modifier = Modifier.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 32.dp),
) {
Column {
characters.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) },
) {
Text(
modifier = Modifier.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = sheet.name,
)
}
}
}
Column {
characters.value.forEach { sheet ->
TextButton(
onClick = { onCreateCharacter() },
onClick = { onCharacter(sheet) },
) {
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__create_action),
maxLines = 1,
text = sheet.name,
)
}
}
}
Spacer(
modifier = Modifier.weight(weight = 1f)
)
TextButton(
onClick = { onNetwork() },
onClick = onCreateCharacter,
) {
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__network_action),
text = stringResource(Res.string.main_page__create_action),
)
}
Column {
TextButton(
onClick = onRollHistory,
) {
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__roll_history_action),
)
}
TextButton(
onClick = onNetwork,
) {
Text(
modifier = Modifier.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = stringResource(Res.string.main_page__network_action),
)
}
}
}
}

View file

@ -3,7 +3,6 @@ package com.pixelized.desktop.lwa.screen.main
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.utils.extention.collectAsState
@ -12,12 +11,6 @@ class MainPageViewModel : ViewModel() {
// using a variable to help with later injection.
private val repository = CharacterSheetRepository
private val _sheet = mutableStateOf<Set<CharacterUio>>(emptySet())
val sheet: State<Set<CharacterUio>> get() = _sheet
private val _create = mutableStateOf<Set<Int>>(emptySet())
val create: State<Set<Int>> get() = _create
val characters: State<List<CharacterUio>>
@Composable
@Stable
@ -31,20 +24,4 @@ class MainPageViewModel : ViewModel() {
)
}
}
fun showCreateCharacterSheet() {
_create.value = _create.value.toMutableSet().apply { add(size) }
}
fun hideCreateCharacterSheet(id: Int) {
_create.value = _create.value.toMutableSet().apply { remove(id) }
}
fun showCharacterSheet(sheet: CharacterUio) {
_sheet.value = _sheet.value.toMutableSet().apply { add(sheet) }
}
fun hideCharacterSheet(sheet: CharacterUio) {
_sheet.value = _sheet.value.toMutableSet().apply { remove(sheet) }
}
}

View file

@ -10,7 +10,7 @@ import com.pixelized.desktop.lwa.business.RollUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.roll.RollHistoryRepository
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
@ -26,7 +26,7 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__success
import org.jetbrains.compose.resources.getString
class RollViewModel : ViewModel() {
private val network = NetworkRepository
private val repository = RollHistoryRepository
private val _roll = mutableStateOf(RollUio(label = "", value = 0))
val roll: State<RollUio> get() = _roll
@ -138,13 +138,11 @@ class RollViewModel : ViewModel() {
value = roll,
)
share(roll = roll)
launch {
repository.share(label = _roll.value.label, roll = roll)
}
}
}
}
}
private fun share(roll: Int) {
network.share(type = "roll", value = "$roll")
}
}

View file

@ -0,0 +1,135 @@
package com.pixelized.desktop.lwa.screen.rollhistory
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
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.runtime.Stable
import androidx.compose.runtime.State
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 androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.navigation.LocalScreenController
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.roll_history__title
import org.jetbrains.compose.resources.stringResource
@Stable
data class RollItemUio(
val from: String,
val label: String,
val roll: Int,
)
@Composable
fun RollHistoryPage(
viewModel: RollHistoryViewModel = viewModel { RollHistoryViewModel() }
) {
val screen = LocalScreenController.current
Surface(
modifier = Modifier.fillMaxSize(),
) {
RollHistoryContent(
modifier = Modifier.fillMaxSize(),
rolls = viewModel.rolls,
onBack = {
screen.popBackStack()
},
)
}
}
@Composable
private fun RollHistoryContent(
modifier: Modifier = Modifier,
rolls: State<List<RollItemUio>>,
onBack: () -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.roll_history__title),
)
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = {
val state = rememberLazyListState()
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = state,
reverseLayout = true,
contentPadding = PaddingValues(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(items = rolls.value) {
RollItem(
roll = it
)
}
}
}
)
}
@Composable
private fun RollItem(
modifier: Modifier = Modifier,
roll: RollItemUio,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Thin,
text = roll.from,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Light,
text = roll.label,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = "${roll.roll}",
)
}
}

View file

@ -0,0 +1,36 @@
package com.pixelized.desktop.lwa.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.network.protocol.RollMessage
import com.pixelized.desktop.lwa.repository.roll.RollHistoryRepository
import kotlinx.coroutines.launch
class RollHistoryViewModel : ViewModel() {
private val repository = RollHistoryRepository
private val _rolls = mutableStateOf((emptyList<RollItemUio>()))
val rolls: State<List<RollItemUio>> get() = _rolls
init {
viewModelScope.launch {
repository.rolls.collect {
(it.value as? RollMessage)?.let { content ->
_rolls.value = _rolls.value.toMutableList().apply {
add(
index = 0,
element = RollItemUio(
from = it.from,
label = content.label,
roll = content.roll
)
)
}
}
}
}
}
}