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.
This commit is contained in:
parent
ba0cc30a1a
commit
0e5fee6771
22 changed files with 958 additions and 161 deletions
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
## Data Store old dependancies not removed properly.
|
||||
-dontwarn okio.AsyncTimeout$Watchdog
|
||||
|
||||
-keep class androidx.compose.runtime.** { *; }
|
||||
-keep class androidx.collection.** { *; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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="roll_page__critical_success">Réussite critique</string>
|
||||
<string name="roll_page__special_success">Réussite spéciale</string>
|
||||
|
|
@ -65,4 +66,19 @@
|
|||
<string name="character_sheet__skills__title">Compétences</string>
|
||||
<string name="character_sheet__occupations_title">Occupations</string>
|
||||
<string name="character_sheet__magics__title">Compétences magiques</string>
|
||||
|
||||
<string name="network__title">Configuration réseau</string>
|
||||
<string name="network__player_name__label">Nom du joueur</string>
|
||||
<string name="network__host__label">host</string>
|
||||
<string name="network__port__label">port</string>
|
||||
<string name="network__socket__connect_action">Se connecter à la table</string>
|
||||
<string name="network__socket__host_action">Héberger la table</string>
|
||||
<string name="network__socket__disconnect_action">Se déconnecter</string>
|
||||
<string name="network__socket__status_state">État de connexion : %1$s</string>
|
||||
<string name="network__socket__status_connected">Connecté</string>
|
||||
<string name="network__socket__status_disconnected">Disconnecté</string>
|
||||
<string name="network__socket__type_state">Type de connexion : %1$s</string>
|
||||
<string name="network__socket__type_server">Serveur</string>
|
||||
<string name="network__socket__type_client">Client</string>
|
||||
<string name="network__socket__type_none">Aucun</string>
|
||||
</resources>
|
||||
|
|
@ -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<WindowController> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NavHostController> {
|
||||
val LocalScreenController = compositionLocalOf<NavHostController> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<NettyApplicationEngine, NettyApplicationEngine.Configuration>
|
||||
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<String>()
|
||||
private val sharedFlow = messageResponseFlow.asSharedFlow()
|
||||
|
||||
private val _player = MutableStateFlow("")
|
||||
val player: StateFlow<String> get() = _player
|
||||
|
||||
private val _status = MutableStateFlow(Status.DISCONNECTED)
|
||||
val status: StateFlow<Status> get() = _status
|
||||
|
||||
private val _type = MutableStateFlow(Type.NONE)
|
||||
val type: StateFlow<Type> 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NetworkPageUio>,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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<NetworkPageUio>
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<RollUio> 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
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" }
|
||||
Loading…
Add table
Add a link
Reference in a new issue