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> <resources>
<string name="main_page__create_action">Créer une feuille de personnage</string> <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__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__critical_success">Réussite critique</string>
<string name="roll_page__special_success">Réussite spéciale</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_server">Serveur</string>
<string name="network__socket__type_client">Client</string> <string name="network__socket__type_client">Client</string>
<string name="network__socket__type_none">Aucun</string> <string name="network__socket__type_none">Aucun</string>
<string name="roll_history__title">Historique des lancés</string>
</resources> </resources>

View file

@ -1,11 +1,16 @@
package com.pixelized.desktop.lwa package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.fillMaxSize 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.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.Window
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.navigation.MainNavHost 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 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 import org.jetbrains.compose.ui.tooling.preview.Preview
val LocalSnackHost = compositionLocalOf<SnackbarHostState> {
error("Local Snack Controller is not yet ready")
}
val LocalWindowController = compositionLocalOf<WindowController> { val LocalWindowController = compositionLocalOf<WindowController> {
error("Local Window Controller is not yet ready") error("Local Window Controller is not yet ready")
} }
@ -24,23 +39,47 @@ val LocalWindowController = compositionLocalOf<WindowController> {
data class WindowController( data class WindowController(
private val onCloseRequest: () -> Unit 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 @Composable
@Preview @Preview
fun ApplicationScope.App() { fun ApplicationScope.App() {
val controller = remember { val controller = remember { WindowController(onCloseRequest = ::exitApplication) }
WindowController( val snackHostState = remember { SnackbarHostState() }
onCloseRequest = ::exitApplication
)
}
CompositionLocalProvider( CompositionLocalProvider(
LocalWindowController provides controller, LocalWindowController provides controller,
LocalSnackHost provides snackHostState,
) { ) {
Window( Window(
onCloseRequest = { onCloseRequest = {
controller.close() controller.closeWindows()
}, },
state = rememberWindowState( state = rememberWindowState(
width = 320.dp + 64.dp, width = 320.dp + 64.dp,
@ -52,9 +91,86 @@ fun ApplicationScope.App() {
Surface( Surface(
modifier = Modifier.fillMaxSize() 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.MainDestination
import com.pixelized.desktop.lwa.navigation.destination.composableMainPage import com.pixelized.desktop.lwa.navigation.destination.composableMainPage
import com.pixelized.desktop.lwa.navigation.destination.composableNetworkPage 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> { val LocalScreenController = compositionLocalOf<NavHostController> {
error("MainNavHost controller is not yet ready") error("MainNavHost controller is not yet ready")
@ -28,6 +30,7 @@ fun MainNavHost(
) { ) {
composableMainPage() composableMainPage()
composableNetworkPage() composableNetworkPage()
composableRollHistory()
} }
} }
} }

View file

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

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.Text
import androidx.compose.material.TopAppBar import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons 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.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -124,7 +123,7 @@ fun CharacterSheetPage(
scope.launch { scope.launch {
viewModel.deleteCharacter(id = sheet.id) viewModel.deleteCharacter(id = sheet.id)
if (screen.popBackStack().not()) { 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 -> content = { paddingValues ->

View file

@ -92,7 +92,7 @@ fun CharacterSheetEditPage(
scope.launch { scope.launch {
viewModel.save() viewModel.save()
if (screen.popBackStack().not()) { 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -27,16 +26,17 @@ import androidx.compose.ui.window.rememberWindowState
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.LocalWindowController import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.WindowController 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.LocalScreenController
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetDestination import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetEditDestination import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.navigation.destination.navigateToNetwork 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 com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetMainNavHost
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__title 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__create_action
import lwacharactersheet.composeapp.generated.resources.main_page__network_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 import org.jetbrains.compose.resources.stringResource
@Stable @Stable
@ -49,6 +49,7 @@ data class CharacterUio(
fun MainPage( fun MainPage(
viewModel: MainPageViewModel = viewModel { MainPageViewModel() }, viewModel: MainPageViewModel = viewModel { MainPageViewModel() },
) { ) {
val window = LocalWindowController.current
val screen = LocalScreenController.current val screen = LocalScreenController.current
Surface( Surface(
@ -64,10 +65,13 @@ fun MainPage(
MainPageContent( MainPageContent(
characters = viewModel.characters, characters = viewModel.characters,
onCharacter = { onCharacter = {
viewModel.showCharacterSheet(sheet = it) window.showCharacterSheet(sheet = it)
}, },
onCreateCharacter = { onCreateCharacter = {
viewModel.showCreateCharacterSheet() window.showCreateCharacterSheet()
},
onRollHistory = {
screen.navigateToRollHistory()
}, },
onNetwork = { onNetwork = {
screen.navigateToNetwork() 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 @Composable
fun MainPageContent( fun MainPageContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
characters: State<List<CharacterUio>>, characters: State<List<CharacterUio>>,
onCharacter: (CharacterUio) -> Unit, onCharacter: (CharacterUio) -> Unit,
onCreateCharacter: () -> Unit, onCreateCharacter: () -> Unit,
onRollHistory: () -> Unit,
onNetwork: () -> Unit, onNetwork: () -> Unit,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Spacer( Column {
modifier = Modifier.weight(weight = 1f) characters.value.forEach { sheet ->
)
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,
)
}
}
}
TextButton( TextButton(
onClick = { onCreateCharacter() }, onClick = { onCharacter(sheet) },
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__create_action), maxLines = 1,
text = sheet.name,
) )
} }
} }
} }
Spacer(
modifier = Modifier.weight(weight = 1f)
)
TextButton( TextButton(
onClick = { onNetwork() }, onClick = onCreateCharacter,
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start, 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.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.utils.extention.collectAsState import com.pixelized.desktop.lwa.utils.extention.collectAsState
@ -12,12 +11,6 @@ class MainPageViewModel : ViewModel() {
// using a variable to help with later injection. // using a variable to help with later injection.
private val repository = CharacterSheetRepository 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>> val characters: State<List<CharacterUio>>
@Composable @Composable
@Stable @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.business.SkillStepUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository 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 com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -26,7 +26,7 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__success
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
class RollViewModel : ViewModel() { class RollViewModel : ViewModel() {
private val network = NetworkRepository private val repository = RollHistoryRepository
private val _roll = mutableStateOf(RollUio(label = "", value = 0)) private val _roll = mutableStateOf(RollUio(label = "", value = 0))
val roll: State<RollUio> get() = _roll val roll: State<RollUio> get() = _roll
@ -138,13 +138,11 @@ class RollViewModel : ViewModel() {
value = roll, 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
)
)
}
}
}
}
}
}