From 0e5fee6771d11408248df28dc390ad082101660a Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Fri, 8 Nov 2024 18:58:47 +0100 Subject: [PATCH] Add basic network feature. Basic implementation of a WebSocket protocol with JSON. Note : obfuscation & release build are deactivated again. Netty (needed for Ktor server) is a nightmare to build in release with or without proguard. Spend more time until now on project configuration that in actual prototyping. --- composeApp/build.gradle.kts | 12 +- composeApp/compose-desktop.pro | 2 - .../composeResources/values/strings.xml | 16 ++ .../kotlin/com/pixelized/desktop/lwa/App.kt | 49 ++++- .../desktop/lwa/navigation/MainNavHost.kt | 11 +- .../destination/NetworkDestination.kt | 26 +++ .../repository/network/NetworkRepository.kt | 170 ++++++++++++++ .../lwa/repository/network/helper/Client.kt | 32 +++ .../lwa/repository/network/helper/Server.kt | 39 ++++ .../repository/network/protocol/Message.kt | 10 + .../characterSheet/CharacterSheetNavHost.kt | 35 +++ .../detail/CharacterSheetPage.kt | 207 ++++++++++-------- .../edit/CharacterSheetEditPage.kt | 10 +- .../desktop/lwa/screen/main/MainPage.kt | 153 +++++++++++-- .../lwa/screen/main/MainPageViewModel.kt | 27 ++- .../lwa/screen/network/NetworkFactory.kt | 24 ++ .../desktop/lwa/screen/network/NetworkPage.kt | 176 +++++++++++++++ .../lwa/screen/network/NetworkViewModel.kt | 62 ++++++ .../desktop/lwa/screen/roll/RollPage.kt | 2 +- .../desktop/lwa/screen/roll/RollViewModel.kt | 9 + .../kotlin/com/pixelized/desktop/lwa/main.kt | 14 +- gradle/libs.versions.toml | 33 +-- 22 files changed, 958 insertions(+), 161 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/destination/NetworkDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Client.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Server.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/Message.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheetNavHost.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 56eaa82..359eca4 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -5,6 +5,8 @@ plugins { alias(libs.plugins.kotlinSerialization) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) +// kotlin("jvm") version "1.9.20" +// alias(libs.plugins.kotlinKtor) } kotlin { @@ -24,6 +26,12 @@ kotlin { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.navigation.compose) implementation(libs.kotlinx.serialization.json) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.websockets) + implementation(libs.ktor.server.core) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.websockets) } commonTest.dependencies { @@ -53,12 +61,12 @@ compose.desktop { includeAllModules = true - // Use system theming fot the app toolbars. + // Use system theming for the app toolbars. jvmArgs("-Dapple.awt.application.appearance=system") } buildTypes.release.proguard { - obfuscate.set(true) // Obfuscation crash at runtime when try to use datastore. + obfuscate.set(false) // Obfuscation doesn't work because of netty. configurationFiles.from(project.file("compose-desktop.pro")) } } diff --git a/composeApp/compose-desktop.pro b/composeApp/compose-desktop.pro index a9bbc0e..f28c79e 100644 --- a/composeApp/compose-desktop.pro +++ b/composeApp/compose-desktop.pro @@ -1,5 +1,3 @@ -## Data Store old dependancies not removed properly. --dontwarn okio.AsyncTimeout$Watchdog -keep class androidx.compose.runtime.** { *; } -keep class androidx.collection.** { *; } diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 896deb3..84f3684 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1,6 +1,7 @@ Créer une feuille de personnage + Configuration réseau Réussite critique Réussite spéciale @@ -65,4 +66,19 @@ Compétences Occupations Compétences magiques + + Configuration réseau + Nom du joueur + host + port + Se connecter à la table + Héberger la table + Se déconnecter + État de connexion : %1$s + Connecté + Disconnecté + Type de connexion : %1$s + Serveur + Client + Aucun \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt index b56fac6..9636519 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt @@ -3,19 +3,58 @@ package com.pixelized.desktop.lwa import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +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.navigation.MainNavHost import com.pixelized.desktop.lwa.theme.LwaTheme import org.jetbrains.compose.ui.tooling.preview.Preview +val LocalWindowController = compositionLocalOf { + error("Local Window Controller is not yet ready") +} + +@Stable +data class WindowController( + private val onCloseRequest: () -> Unit +) { + fun close() = onCloseRequest() +} + @Composable @Preview -fun App() { - LwaTheme { - Surface( - modifier = Modifier.fillMaxSize() +fun ApplicationScope.App() { + val controller = remember { + WindowController( + onCloseRequest = ::exitApplication + ) + } + CompositionLocalProvider( + LocalWindowController provides controller, + ) { + Window( + onCloseRequest = { + controller.close() + }, + state = rememberWindowState( + width = 320.dp + 64.dp, + height = 900.dp, + ), + title = "LwaCharacterSheet", ) { - MainNavHost() + LwaTheme { + Surface( + modifier = Modifier.fillMaxSize() + ) { + MainNavHost() + } + } } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/MainNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/MainNavHost.kt index 49ecf42..22d6c92 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/MainNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/MainNavHost.kt @@ -7,11 +7,10 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import com.pixelized.desktop.lwa.navigation.destination.MainDestination -import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetEditPage -import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetPage import com.pixelized.desktop.lwa.navigation.destination.composableMainPage +import com.pixelized.desktop.lwa.navigation.destination.composableNetworkPage -val LocalScreen = compositionLocalOf { +val LocalScreenController = compositionLocalOf { error("MainNavHost controller is not yet ready") } @@ -21,16 +20,14 @@ fun MainNavHost( startDestination: String = MainDestination.navigationRoute(), ) { CompositionLocalProvider( - LocalScreen provides controller, + LocalScreenController provides controller, ) { NavHost( navController = controller, startDestination = startDestination, ) { composableMainPage() - - composableCharacterSheetPage() - composableCharacterSheetEditPage() + composableNetworkPage() } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/destination/NetworkDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/destination/NetworkDestination.kt new file mode 100644 index 0000000..edd3b41 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/destination/NetworkDestination.kt @@ -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.network.NetworkPage + +object NetworkDestination { + private const val ROUTE = "network" + + fun baseRoute() = ROUTE + fun navigationRoute() = ROUTE +} + +fun NavGraphBuilder.composableNetworkPage() { + composable( + route = NetworkDestination.baseRoute(), + ) { + NetworkPage() + } +} + +fun NavHostController.navigateToNetwork() { + val route = NetworkDestination.navigationRoute() + navigate(route = route) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt new file mode 100644 index 0000000..d39b0e1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt @@ -0,0 +1,170 @@ +package com.pixelized.desktop.lwa.repository.network + +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 io.ktor.client.HttpClient +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.netty.NettyApplicationEngine +import io.ktor.websocket.Frame +import io.ktor.websocket.readText +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement + +typealias Server = EmbeddedServer +typealias Client = HttpClient + +object NetworkRepository { + private val scope = CoroutineScope(Dispatchers.IO) + private var networkJob: Job? = null + private var server: Server? = null + private var client: Client? = null + + private val messageResponseFlow = MutableSharedFlow() + private val sharedFlow = messageResponseFlow.asSharedFlow() + + private val _player = MutableStateFlow("") + val player: StateFlow get() = _player + + private val _status = MutableStateFlow(Status.DISCONNECTED) + val status: StateFlow get() = _status + + private val _type = MutableStateFlow(Type.NONE) + val type: StateFlow get() = _type + + fun onPlayerNameChange(player: String) { + _player.value = player + } + + fun host( + port: Int, + ) { + _type.value = Type.SERVER + _status.value = Status.CONNECTED + + server = server(port = port) { + println("Server launched") + + val job = launch { + sharedFlow.collect { message -> + println("Broadcast: $message") + send(Frame.Text(message)) + } + } + + runCatching { + incoming.consumeEach { frame -> + if (frame is Frame.Text) { + val receivedText = frame.readText() + messageResponseFlow.emit(receivedText) + } + } + }.onFailure { exception -> + // TODO + println("WebSocket exception: ${exception.localizedMessage}") + }.also { + job.cancel() + } + } + + networkJob?.cancel() + networkJob = scope.launch { + try { + server?.start(wait = true) + } catch (exception: Exception) { + // TODO + println("WebSocket exception: ${exception.localizedMessage}") + } finally { + println("Server close") + _type.value = Type.NONE + _status.value = Status.DISCONNECTED + } + } + } + + fun connect( + host: String, + port: Int, + ) { + client = client() + + networkJob?.cancel() + networkJob = scope.launch { + try { + client?.connectWebSocket(host = host, port = port) { + _type.value = Type.CLIENT + _status.value = Status.CONNECTED + println("Client launched") + + val job = launch { + sharedFlow.collect { message -> + println("Send: $message") + send(Frame.Text(message)) + } + } + + runBlocking { + incoming.consumeEach { frame -> + if (frame is Frame.Text) { + val receivedText = frame.readText() + println("client received: $receivedText") + } + } + }.also { + job.cancel() + } + } + } catch (exception: Exception) { + // TODO + println("WebSocket exception: ${exception.localizedMessage}") + } finally { + println("Client close") + _type.value = Type.NONE + _status.value = Status.DISCONNECTED + } + } + } + + fun disconnect() { + networkJob?.cancel() + scope.launch { + server?.stop() + client?.close() + } + } + + fun share( + type: String, + value: String, + ) { + 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()) + } + } + } + + enum class Status { + CONNECTED, + DISCONNECTED + } + + enum class Type { + CLIENT, + SERVER, + NONE, + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Client.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Client.kt new file mode 100644 index 0000000..76884b1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Client.kt @@ -0,0 +1,32 @@ +package com.pixelized.desktop.lwa.repository.network.helper + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.http.HttpMethod + +// https://ktor.io/docs/client-websockets.html#handle-session +fun client(): HttpClient { + val client = HttpClient(CIO) { + install(WebSockets) { + pingIntervalMillis = 20_000 + } + } + return client +} + +suspend fun HttpClient.connectWebSocket( + host: String, + port: Int, + block: suspend DefaultClientWebSocketSession.() -> Unit +) { + webSocket( + method = HttpMethod.Get, + host = host, + port = port, + path = "/ws", + block = block, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Server.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Server.kt new file mode 100644 index 0000000..e5c321d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/helper/Server.kt @@ -0,0 +1,39 @@ +package com.pixelized.desktop.lwa.repository.network.helper + +import io.ktor.server.application.install +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.netty.NettyApplicationEngine +import io.ktor.server.routing.routing +import io.ktor.server.websocket.DefaultWebSocketServerSession +import io.ktor.server.websocket.WebSockets +import io.ktor.server.websocket.pingPeriod +import io.ktor.server.websocket.timeout +import io.ktor.server.websocket.webSocket +import kotlin.time.Duration.Companion.seconds + +// https://ktor.io/docs/server-websockets.html#handle-multiple-session +fun server( + port: Int = 8080, + handler: suspend DefaultWebSocketServerSession.() -> Unit +): EmbeddedServer { + return embeddedServer( + factory = Netty, + port = port, + module = { + install(WebSockets) { + pingPeriod = 15.seconds + timeout = 15.seconds + maxFrameSize = Long.MAX_VALUE + masking = false + } + routing { + webSocket( + path = "/ws", + handler = handler, + ) + } + }, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/Message.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/Message.kt new file mode 100644 index 0000000..1048cc4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/protocol/Message.kt @@ -0,0 +1,10 @@ +package com.pixelized.desktop.lwa.repository.network.protocol + +import kotlinx.serialization.Serializable + +@Serializable +class Message( + val from: String, + val type: String, + val value: String, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheetNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheetNavHost.kt new file mode 100644 index 0000000..5257758 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheetNavHost.kt @@ -0,0 +1,35 @@ +package com.pixelized.desktop.lwa.screen.characterSheet + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.pixelized.desktop.lwa.navigation.LocalScreenController +import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetEditPage +import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetPage + +@Composable +fun CharacterSheetMainNavHost( + controller: NavHostController = rememberNavController(), + startDestination: String, +) { + CompositionLocalProvider( + LocalScreenController provides controller, + ) { + Surface( + modifier = Modifier.fillMaxSize(), + ) { + NavHost( + navController = controller, + startDestination = startDestination, + ) { + composableCharacterSheetPage() + composableCharacterSheetEditPage() + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt index cb3f03d..8677308 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt @@ -4,13 +4,12 @@ import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -36,14 +35,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel +import com.pixelized.desktop.lwa.LocalWindowController import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.composable.overlay.BlurOverlay import com.pixelized.desktop.lwa.composable.overlay.BlurOverlayViewModel -import com.pixelized.desktop.lwa.navigation.LocalScreen +import com.pixelized.desktop.lwa.navigation.LocalScreenController import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit import com.pixelized.desktop.lwa.screen.roll.RollPage import com.pixelized.desktop.lwa.screen.roll.RollViewModel @@ -95,7 +94,8 @@ fun CharacterSheetPage( overlayViewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() }, rollViewModel: RollViewModel = viewModel { RollViewModel() }, ) { - val screen = LocalScreen.current + val window = LocalWindowController.current + val screen = LocalScreenController.current val scope = rememberCoroutineScope() Surface( @@ -123,7 +123,9 @@ fun CharacterSheetPage( onDelete = { scope.launch { viewModel.deleteCharacter(id = sheet.id) - screen.popBackStack() + if (screen.popBackStack().not()) { + window.close() + } } }, onCharacteristic = { characteristic -> @@ -148,12 +150,10 @@ fun CharacterSheetPage( } } -@OptIn(ExperimentalLayoutApi::class) @Composable fun CharacterSheetPageContent( modifier: Modifier = Modifier, scrollState: ScrollState = rememberScrollState(), - width: Dp = 320.dp, characterSheet: CharacterSheetPageUio, onBack: () -> Unit, onEdit: () -> Unit, @@ -163,6 +163,7 @@ fun CharacterSheetPageContent( onRoll: (roll: CharacterSheetPageUio.Roll) -> Unit, ) { Scaffold( + modifier = modifier, topBar = { TopAppBar( title = { @@ -203,115 +204,131 @@ fun CharacterSheetPageContent( ) }, content = { paddingValues -> - Column( + Row( modifier = Modifier - .verticalScroll(state = scrollState).padding(all = 16.dp) + .verticalScroll(state = scrollState) .padding(paddingValues) - .then(other = modifier), - verticalArrangement = Arrangement.spacedBy(space = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(space = 16.dp), ) { - FlowRow( - maxItemsInEachRow = 3, - horizontalArrangement = Arrangement.spacedBy( - space = 16.dp, - alignment = Alignment.CenterHorizontally, - ), + Column( + modifier = Modifier + .fillMaxHeight() + .width(100.dp), verticalArrangement = Arrangement.spacedBy(space = 16.dp), ) { characterSheet.characteristics.forEach { Stat( - modifier = Modifier.width(width = width / 3 - 32.dp) - .height(height = 112.dp), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 120.dp), characteristic = it, onClick = { onCharacteristic(it) }, ) } } - DecoratedBox( - modifier = Modifier.width(width = width).padding(vertical = 8.dp), + Column( + modifier = Modifier + .fillMaxHeight() + .weight(2f / 3f), + verticalArrangement = Arrangement.spacedBy(space = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column { - Text( - modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = stringResource(Res.string.character_sheet__sub_characteristics__title), - ) - characterSheet.subCharacteristics.forEach { - Characteristics( - modifier = Modifier.fillMaxWidth(), - characteristic = it, + DecoratedBox( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(Res.string.character_sheet__sub_characteristics__title), ) + characterSheet.subCharacteristics.forEach { + Characteristics( + modifier = Modifier.fillMaxWidth(), + characteristic = it, + ) + } } } - } - DecoratedBox( - modifier = Modifier.width(width = width).padding(vertical = 8.dp), - ) { - Column { - Text( - modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = stringResource(Res.string.character_sheet__skills__title), - ) - characterSheet.skills.forEach { - Skill( - modifier = Modifier.fillMaxWidth(), - label = it.label, - value = it.value, - onClick = { onSkill(it) }, + DecoratedBox( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(Res.string.character_sheet__skills__title), ) + characterSheet.skills.forEach { + Skill( + modifier = Modifier.fillMaxWidth(), + label = it.label, + value = it.value, + onClick = { onSkill(it) }, + ) + } } } - } - DecoratedBox( - modifier = Modifier.width(width = width).padding(vertical = 8.dp), - ) { - Column { - Text( - modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = stringResource(Res.string.character_sheet__occupations_title), - ) - characterSheet.occupations.forEach { - Skill( - modifier = Modifier.fillMaxWidth(), - label = it.label, - value = it.value, - onClick = { onSkill(it) }, + DecoratedBox( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(Res.string.character_sheet__occupations_title), ) + characterSheet.occupations.forEach { + Skill( + modifier = Modifier.fillMaxWidth(), + label = it.label, + value = it.value, + onClick = { onSkill(it) }, + ) + } } } - } - DecoratedBox( - modifier = Modifier.width(width = width).padding(vertical = 8.dp), - ) { - Column { - Text( - modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = stringResource(Res.string.character_sheet__magics__title), - ) - characterSheet.magics.forEach { - Skill( - modifier = Modifier.fillMaxWidth(), - label = it.label, - value = it.value, - onClick = { onSkill(it) }, + DecoratedBox( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(Res.string.character_sheet__magics__title), ) + characterSheet.magics.forEach { + Skill( + modifier = Modifier.fillMaxWidth(), + label = it.label, + value = it.value, + onClick = { onSkill(it) }, + ) + } } } - } - characterSheet.rolls.forEach { - Roll( - modifier = Modifier.fillMaxWidth(), - label = it.label, - onClick = { onRoll(it) }, - ) + characterSheet.rolls.forEach { + Roll( + modifier = Modifier.fillMaxWidth(), + label = it.label, + onClick = { onRoll(it) }, + ) + } } } } @@ -326,7 +343,9 @@ private fun Stat( onClick: () -> Unit, ) { DecoratedBox( - modifier = Modifier.clickable(onClick = onClick).padding(paddingValues = paddingValues) + modifier = Modifier + .clickable(onClick = onClick) + .padding(paddingValues = paddingValues) .then(other = modifier), ) { Text( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt index fdf9161..a4af668 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt @@ -29,8 +29,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel +import com.pixelized.desktop.lwa.LocalWindowController import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox -import com.pixelized.desktop.lwa.navigation.LocalScreen +import com.pixelized.desktop.lwa.navigation.LocalScreenController import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form import kotlinx.coroutines.launch @@ -75,7 +76,8 @@ fun CharacterSheetEditPage( ) }, ) { - val screen = LocalScreen.current + val window = LocalWindowController.current + val screen = LocalScreenController.current val scope = rememberCoroutineScope() Surface( @@ -89,7 +91,9 @@ fun CharacterSheetEditPage( onSave = { scope.launch { viewModel.save() - screen.popBackStack() + if (screen.popBackStack().not()) { + window.close() + } } }, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt index a8bcbaa..f7ee73e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt @@ -3,26 +3,40 @@ 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 +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.State +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.rememberWindowState import androidx.lifecycle.viewmodel.compose.viewModel -import com.pixelized.desktop.lwa.navigation.LocalScreen -import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheet -import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit +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.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 org.jetbrains.compose.resources.stringResource @Stable @@ -35,62 +49,165 @@ data class CharacterUio( fun MainPage( viewModel: MainPageViewModel = viewModel { MainPageViewModel() }, ) { - val screen = LocalScreen.current + val screen = LocalScreenController.current - Surface { + Surface( + modifier = Modifier.fillMaxSize(), + ) { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .verticalScroll(state = rememberScrollState()) + .fillMaxSize() + .padding(horizontal = 24.dp), contentAlignment = Alignment.Center, ) { MainPageContent( characters = viewModel.characters, onCharacter = { - screen.navigateToCharacterSheet(id = it.id) + viewModel.showCharacterSheet(sheet = it) }, onCreateCharacter = { - screen.navigateToCharacterSheetEdit() + viewModel.showCreateCharacterSheet() + }, + onNetwork = { + screen.navigateToNetwork() }, ) } } + + HandleCharacterSheet( + sheets = viewModel.sheet, + onCloseRequest = { viewModel.hideCharacterSheet(sheet = it) } + ) + + HandleCharacterSheetCreation( + sheets = viewModel.create, + onCloseRequest = { viewModel.hideCreateCharacterSheet(id = it) }, + ) } +@Composable +fun HandleCharacterSheet( + sheets: State>, + 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>, + 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>, onCharacter: (CharacterUio) -> Unit, onCreateCharacter: () -> Unit, + onNetwork: () -> Unit, ) { Column( - modifier = modifier.padding(horizontal = 24.dp), - verticalArrangement = Arrangement.spacedBy(space = 32.dp), + modifier = modifier, ) { - Column { - characters.value.forEach { sheet -> + 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, + ) + } + } + } + TextButton( - onClick = { onCharacter(sheet) }, + onClick = { onCreateCharacter() }, ) { 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), ) } } } - + Spacer( + modifier = Modifier.weight(weight = 1f) + ) TextButton( - onClick = { onCreateCharacter() }, + onClick = { onNetwork() }, ) { Text( modifier = Modifier.fillMaxWidth(), maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Start, - text = stringResource(Res.string.main_page__create_action), + text = stringResource(Res.string.main_page__network_action), ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPageViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPageViewModel.kt index d627306..0947b2a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPageViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPageViewModel.kt @@ -3,18 +3,25 @@ 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 class MainPageViewModel : ViewModel() { // using a variable to help with later injection. - private val characterSheetRepository = CharacterSheetRepository + private val repository = CharacterSheetRepository + + private val _sheet = mutableStateOf>(emptySet()) + val sheet: State> get() = _sheet + + private val _create = mutableStateOf>(emptySet()) + val create: State> get() = _create val characters: State> @Composable @Stable - get() = characterSheetRepository + get() = repository .characterSheetFlow() .collectAsState { sheets -> sheets.map { sheet -> @@ -24,4 +31,20 @@ 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) } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt new file mode 100644 index 0000000..dbfbc2a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkFactory.kt @@ -0,0 +1,24 @@ +package com.pixelized.desktop.lwa.screen.network + +import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status +import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Type + +class NetworkFactory { + + fun convertToUio( + player: String, + status: Status, + type: Type, + host: String, + port: Int, + ): NetworkPageUio { + return NetworkPageUio( + player = player, + host = host, + port = "$port", + enableFields = status == Status.DISCONNECTED, + enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0, + enableCancel = type != Type.NONE && status == Status.CONNECTED + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt new file mode 100644 index 0000000..8a1f644 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkPage.kt @@ -0,0 +1,176 @@ +package com.pixelized.desktop.lwa.screen.network + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TextField +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.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.network__host__label +import lwacharactersheet.composeapp.generated.resources.network__player_name__label +import lwacharactersheet.composeapp.generated.resources.network__port__label +import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action +import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action +import lwacharactersheet.composeapp.generated.resources.network__socket__host_action +import lwacharactersheet.composeapp.generated.resources.network__title +import org.jetbrains.compose.resources.stringResource + +@Stable +data class NetworkPageUio( + val player: String, + val host: String, + val port: String, + val enableFields: Boolean, + val enableActions: Boolean, + val enableCancel: Boolean, +) + +@Composable +fun NetworkPage( + viewModel: NetworkViewModel = viewModel { NetworkViewModel() }, +) { + val screen = LocalScreenController.current + + Surface( + modifier = Modifier.fillMaxSize(), + ) { + NetworkContent( + modifier = Modifier.fillMaxSize(), + player = viewModel.network, + onBack = { screen.popBackStack() }, + onPlayerChange = viewModel::onPlayerNameChange, + onHostChange = viewModel::onHostChange, + onPortChange = viewModel::onPortChange, + onConnect = viewModel::connect, + onHost = viewModel::host, + onDisconnect = viewModel::disconnect, + ) + } +} + +@Composable +private fun NetworkContent( + modifier: Modifier = Modifier, + scrollState: ScrollState = rememberScrollState(), + player: State, + onBack: () -> Unit, + onPlayerChange: (String) -> Unit, + onHostChange: (String) -> Unit, + onPortChange: (String) -> Unit, + onConnect: () -> Unit, + onHost: () -> 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, + ) + } + } + ) + }, + 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.enableActions, + onClick = onHost, + ) { + Text(text = stringResource(Res.string.network__socket__host_action)) + } + + TextButton( + enabled = player.value.enableCancel, + onClick = onDisconnect, + ) { + Text(text = stringResource(Res.string.network__socket__disconnect_action)) + } + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt new file mode 100644 index 0000000..1ce3c31 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/network/NetworkViewModel.kt @@ -0,0 +1,62 @@ +package com.pixelized.desktop.lwa.screen.network + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel +import com.pixelized.desktop.lwa.repository.network.NetworkRepository + +class NetworkViewModel : ViewModel() { + private val repository = NetworkRepository + private val factory = NetworkFactory() + + private val host = mutableStateOf("localhost") + private val port = mutableStateOf(27030) + val network: State + @Composable + @Stable + get() { + val player = repository.player.collectAsState() + val status = repository.status.collectAsState() + val type = repository.type.collectAsState() + return remember { + derivedStateOf { + factory.convertToUio( + player = player.value, + status = status.value, + type = type.value, + host = host.value, + port = port.value, + ) + } + } + } + + fun onPlayerNameChange(player: String) { + repository.onPlayerNameChange(player = player) + } + + fun onPortChange(port: String) { + this.port.value = port.toIntOrNull() ?: 0 + } + + fun onHostChange(host: String) { + this.host.value = host + } + + fun host() { + repository.host(port = port.value) + } + + fun connect() { + repository.connect(host = host.value, port = port.value) + } + + fun disconnect() { + repository.disconnect() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt index 7f12bc6..189e2de 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt @@ -109,7 +109,7 @@ fun RollPage( this.alpha = 0.8f this.rotationZ = viewModel.rollRotation.value }, - tint = MaterialTheme.colors.onSurface, + tint = MaterialTheme.colors.primary, painter = painterResource(Res.drawable.ic_d20_32dp), contentDescription = null, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt index 60bcf17..e3b83d1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt @@ -10,6 +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.screen.characterSheet.detail.CharacterSheetPageUio import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope @@ -25,6 +26,8 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__success import org.jetbrains.compose.resources.getString class RollViewModel : ViewModel() { + private val network = NetworkRepository + private val _roll = mutableStateOf(RollUio(label = "", value = 0)) val roll: State get() = _roll @@ -134,8 +137,14 @@ class RollViewModel : ViewModel() { } ?: "", value = roll, ) + + share(roll = roll) } } } } + + private fun share(roll: Int) { + network.share(type = "roll", value = "$roll") + } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/main.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/main.kt index af06c8c..bdbba3a 100644 --- a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/main.kt @@ -1,21 +1,9 @@ package com.pixelized.desktop.lwa -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import androidx.compose.ui.window.rememberWindowState fun main() { application { - Window( - onCloseRequest = ::exitApplication, - state = rememberWindowState( - width = 320.dp + 64.dp, - height = 900.dp, - ), - title = "LwaCharacterSheet", - ) { - App() - } + App() } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64e71f8..e2f7e34 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,22 +6,27 @@ junit = "4.13.2" compose-multiplatform = "1.7.0" androidx-lifecycle = "2.8.3" androidx-navigation = "2.8.0-alpha10" - -[libraries] -# Test -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } -junit = { group = "junit", name = "junit", version.ref = "junit" } - -# Compose -androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } -androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } -androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } -kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } -kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" } +ktor_version = "3.0.1" [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinKtor = { id = "io.ktor.plugin", version.ref = "ktor_version"} + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } +kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" } +ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor_version" } +ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor_version" } +ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor_version" } +ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor_version" } +ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor_version" } +ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor_version" } \ No newline at end of file