more structure to forms and views

shared ui got an interface to demo the components
This commit is contained in:
2026-08-28 16:20:35 -04:00
parent d1ef2956d8
commit 2e47b8bfce
50 changed files with 941 additions and 610 deletions
-3
View File
@@ -1,6 +1,5 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
// alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.compose) alias(libs.plugins.compose)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
@@ -9,8 +8,6 @@ plugins {
dependencies { dependencies {
implementation(project(":shared")) implementation(project(":shared"))
implementation(project(":shared_ui")) implementation(project(":shared_ui"))
// implementation(libs.compose.icons.material)
// implementation("com.composables:icons-material-icons-filled-cmp:2.2.1")
implementation(libs.kotlinx.serialization) implementation(libs.kotlinx.serialization)
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
@@ -9,6 +9,7 @@ import dev.geekback.dopewars.shared.api.TravelRequest
class ClientState(player: Player) { class ClientState(player: Player) {
var player by mutableStateOf(player) var player by mutableStateOf(player)
var hasLoggedIn by mutableStateOf(false)
var apiUrl: String = "http://localhost:8080" var apiUrl: String = "http://localhost:8080"
private var api: ApiClient = ApiClient() private var api: ApiClient = ApiClient()
@@ -21,9 +22,15 @@ class ClientState(player: Player) {
suspend fun login() : Player { suspend fun login() : Player {
val p = this.api.login("http://localhost:8080") val p = this.api.login("http://localhost:8080")
println("Logged in as ${p.name} / ${p.id}") println("Logged in as ${p.name} / ${p.id}")
this.hasLoggedIn = true
return p return p
} }
suspend fun logout() {
this.hasLoggedIn = false
}
suspend fun updateMarket() { suspend fun updateMarket() {
println("Updating market") println("Updating market")
this.player.lastMarket = this.api.market(this.player) this.player.lastMarket = this.api.market(this.player)
@@ -37,8 +44,7 @@ class ClientState(player: Player) {
} }
fun isLoggedIn(): Boolean { fun isLoggedIn(): Boolean {
println("Testing if user is logged in - ${this.player.id.isNotEmpty()}") return this.hasLoggedIn
return this.player.id.isNotEmpty()
} }
fun refreshApi(url: String) { fun refreshApi(url: String) {
@@ -48,7 +54,7 @@ class ClientState(player: Player) {
suspend fun travel(player: Player, destination: TravelRequest) { suspend fun travel(player: Player, destination: TravelRequest) {
val result = this.api.travel("http://localhost:8080", player.id, destination) this.api.travel("http://localhost:8080", player.id, destination)
} }
@@ -1,180 +0,0 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Locations
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.MarketRequest
import dev.geekback.dopewars.shared.api.MarketResult
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.shared.api.PlayerNameUpdateResponse
import dev.geekback.dopewars.shared.api.TradeAction
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.DopeWarsUi
import kotlinx.coroutines.launch
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "DopeWars Dev Client"
) {
DemoApp()
}
}
data class ApiData(
var marketRequest: MarketRequest,
var marketResult: MarketResult,
var playerNameUpdateRequest: PlayerNameUpdateRequest,
var playerNameUpadteResponse: PlayerNameUpdateResponse,
var tradeAction: TradeAction,
var tradeRequest: TradeRequest,
var travelRequest: TravelRequest,
var travelResult: TravelResult
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DemoApp() {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
var currentScreen by remember { mutableStateOf(Screens.HOME) }
var drug by remember { mutableStateOf(DrugInfo(drug = Drug.WEED, minValue = 100.0f, maxValue = 1000.0f)) }
var player by remember { mutableStateOf(Player(id = "____", name = "TBD")) }
var location by remember { mutableStateOf(Location(name = "LocationDefault"))}
var market by remember { mutableStateOf(Market(
location = Location(name = "MarketDefault"),
prices = mapOf()
))}
val ui = DopeWarsUi.new()
val travelResultLocation = Locations.random();
var api by remember { mutableStateOf(ApiData(
marketRequest = MarketRequest(
location = Location(name = "MarketRequestLocation")
),
marketResult = MarketResult(
market = Market(location = Location(name = "MarketResultLocation"),
prices = emptyMap()
)
),
playerNameUpdateRequest = PlayerNameUpdateRequest("newPlayerName"),
playerNameUpadteResponse = PlayerNameUpdateResponse(
updatedPlayer = Player(id = "____", name = "updatedPlayerName")
),
tradeAction = TradeAction.BUY,
tradeRequest = TradeRequest(
action = TradeAction.BUY,
drug = Drug.random(),
quantity = 10
),
travelRequest = TravelRequest(
destination = Locations.random()
),
travelResult = TravelResult(
location = travelResultLocation,
day = 1,
market = Market(location = travelResultLocation, mapOf())
)
)) }
MaterialTheme {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Button(onClick = {
currentScreen = Screens.HOME
scope.launch { drawerState.close() }
}) { Text("Home")}
Button( onClick = {
currentScreen = Screens.DRUG
scope.launch { drawerState.close() }
}) { Text("Drug") }
Button( onClick = {
currentScreen = Screens.PLAYER
scope.launch { drawerState.close() }
}) { Text("Player") }
Button(onClick = {
currentScreen = Screens.LOCATION
scope.launch { drawerState.close() }
}) { Text("Location") }
Button( onClick = {
currentScreen = Screens.API
scope.launch { drawerState.close() }
}) { Text("API") }
}
}
) {
// Main content
when (currentScreen) {
Screens.HOME -> {
Column {
ui.player.view(player)
// ui.drug.view(drug)
}
}
Screens.DRUG -> {
// ui.drug.form(drug) {
// drug = it
// }
}
Screens.PLAYER -> {
ui.player.form(player) {
player = it
}
}
Screens.LOCATION -> {
ui.location.form(location) {
location = it
}
}
Screens.MARKET -> {
ui.market.form(market) {
market = it
}
}
Screens.API -> {
Column {
ui.api.marketRequest.form(api.marketRequest) { api.marketRequest = it }
ui.api.marketResponse.form(api.marketResult) { api.marketResult = it }
ui.api.playerNameUpdateRequest.form(api.playerNameUpdateRequest) { api.playerNameUpdateRequest = it }
ui.api.playerNameUpdateResponse.form(api.playerNameUpadteResponse) { api.playerNameUpadteResponse = it }
ui.api.travelRequest.form(api.travelRequest) { api.travelRequest = it }
ui.api.travelResponse.form(api.travelResult) { api.travelResult = it}
ui.api.tradeRequest.form(api.tradeRequest) { api.tradeRequest = it}
}
}
}
}
}
}
@@ -1,131 +0,0 @@
package dev.geekback.dopewars.desktop
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.ui.AppTheme
fun main() {
val state = ClientState(Player(id = "", name = "TestClient"))
// var drug = Drug("TestDrug", minPrice = 10.0f, maxPrice = 100.0f)
var player = Player(id = "default", name = "defaultName")
var location = Location(name = "DefaultLocation")
application {
var currentScreen = remember { Screens.DRUG }
val scope = rememberCoroutineScope()
// state.updateApiUrl("http://localhost:8080")
// val scaffoldState = rememberScaffoldState(drawerState = DrawerState(DrawerValue.Closed))
// var menuVisible by remember { mutableStateOf(false) }
Window(onCloseRequest = ::exitApplication, title = "DopeWars") {
/*
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
Button(onClick = {
currentScreen = Screens.DRUG
scope.launch {
scaffoldState.drawerState.close()
}
}) { Text("Drug")}
Button(onClick = {
currentScreen = Screens.PLAYER
scope.launch {
scaffoldState.drawerState.close()
}
}) { Text("Player") }
},
topBar = {
TopAppBar(
title = {
Text("Title")
},
navigationIcon = {
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(imageVector = MaterialIcons.Filled.Menu , contentDescription = "Menu")
}
})
}
) {
*/
AppTheme {
Text("Test123")
Column(modifier = Modifier.fillMaxSize()) {
/*
when (currentScreen) {
Screens.DRUG -> {
drugForm(drug) {
drug = it
}
drugView(drug)
}
Screens.PLAYER -> {
playerView(player)
}
Screens.LOCATION -> {
locationView(location)
locationForm(location) {
location = it
}
}
Screens.INVENTORY -> {
inventoryView(emptyList())
}
Screens.MARKET -> {
marketView(Market(Location("Unknown Location"), emptyMap()))
}
}
*/
}
}
}
/*
println("Session ID = ${state.player.id}")
Column {
Text("DopeWars Desktop Client")
configPanel(config = state, onUrlChanged = {
state.updateApiUrl(it)
})
PlayerForm(
initialState = state.player,
onRequestLogin = {
scope.launch {
state.login()
}
},
onRequestPlayerRename = { newName ->
scope.launch {
state.renamePlayer(newName)
}
},
onGetMarket = {
scope.launch {
state.updateMarket()
}
},
onRefresh = {
scope.launch {
state.refreshPlayer()
}
}
)
}
*/
}
}
@@ -10,6 +10,7 @@ import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -21,6 +22,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import compose.icons.FeatherIcons
import compose.icons.feathericons.LogIn
import dev.geekback.dopewars.ui.AppTheme import dev.geekback.dopewars.ui.AppTheme
import dev.geekback.dopewars.ui.PlayerUi import dev.geekback.dopewars.ui.PlayerUi
import dev.geekback.dopewars.ui.TravelUi import dev.geekback.dopewars.ui.TravelUi
@@ -51,18 +54,31 @@ fun App() {
ModalDrawerSheet { ModalDrawerSheet {
if (state.isLoggedIn()) { if (state.isLoggedIn()) {
// Display the current player state // Display the current player state
PlayerUi().view(state.player) PlayerUi().view(state.player) {}
} else {
if (state.isLoggedIn()) {
NavigationDrawerItem(
label = {
Row {
Text("Logout")
}
},
selected = false,
onClick = {
scope.launch {
state.logout()
}
}
)
} else { } else {
NavigationDrawerItem( NavigationDrawerItem(
label = { label = {
Row { Row {
/*
Icon( Icon(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
imageVector = , imageVector = FeatherIcons.LogIn,
contentDescription = "Login Icon" contentDescription = "Login Icon"
) )
*/
Text( Text(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
text = "Login" text = "Login"
@@ -79,6 +95,7 @@ fun App() {
} }
} }
} }
}
) { ) {
if (state.isLoggedIn()) { if (state.isLoggedIn()) {
mainWindow(state) mainWindow(state)
@@ -91,13 +108,12 @@ fun App() {
} }
} }
@Composable @Composable
fun mainWindow(state: ClientState) { fun mainWindow(state: ClientState) {
val scope = rememberCoroutineScope(); val scope = rememberCoroutineScope()
var showingTravelWizard by remember { mutableStateOf(false) } var showingTravelWizard by remember { mutableStateOf(false) }
PlayerUi().view(state.player) PlayerUi().view(state.player) {}
Row { Row {
Button(onClick = { Button(onClick = {
println("Transactions") println("Transactions")
+3
View File
@@ -1,2 +1,5 @@
org.gradle.configuration-cache=true org.gradle.configuration-cache=true
org.gradle.jvmargs=-Xmx12g org.gradle.jvmargs=-Xmx12g
org.gradle.tooling.parallel=true
org.gradle.parallel=true
org.gradle.daemon=true
+3 -1
View File
@@ -1,5 +1,5 @@
[versions] [versions]
ktor = "3.5.1" ktor = "3.5.2"
kotlin = "2.4.0" kotlin = "2.4.0"
compose = "1.11.1" compose = "1.11.1"
compose-material3 = "1.11.0-alpha07" compose-material3 = "1.11.0-alpha07"
@@ -34,6 +34,8 @@ ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref =
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" } ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
ktor-server-status-pages = { group ="io.ktor", name="ktor-server-status-pages", version.ref="ktor" } ktor-server-status-pages = { group ="io.ktor", name="ktor-server-status-pages", version.ref="ktor" }
ktor-server-logging = { group = "io.ktor", name="ktor-server-call-logging", version.ref="ktor" } ktor-server-logging = { group = "io.ktor", name="ktor-server-call-logging", version.ref="ktor" }
ktor-server-cors = { group = "io.ktor", name="ktor-server-cors", version.ref="ktor" }
# KTOR client # KTOR client
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref="ktor" } ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref="ktor" }
+3
View File
@@ -46,6 +46,9 @@ dependencies {
// Websockets // Websockets
implementation(libs.ktor.server.websockets) implementation(libs.ktor.server.websockets)
// CORS
implementation(libs.ktor.server.cors)
// General Use // General Use
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
+2 -9
View File
@@ -698,20 +698,13 @@
}, },
"quantity" : { } "quantity" : { }
} }
};
defs["Location"] = {
"title" : "Location",
"required" : [ "name" ],
"properties" : {
"name" : { }
}
}; };
defs["Market"] = { defs["Market"] = {
"title" : "Market", "title" : "Market",
"required" : [ "location", "prices" ], "required" : [ "location", "prices" ],
"properties" : { "properties" : {
"location" : { "location" : {
"$ref" : "#/components/schemas/Location" "enum" : [ "BROOKLYN", "BRONX", "STATTEN_ISLAND", "QUEENS" ]
}, },
"prices" : { "prices" : {
"additionalProperties" : { } "additionalProperties" : { }
@@ -737,7 +730,7 @@
"debt" : { }, "debt" : { },
"health" : { }, "health" : { },
"location" : { "location" : {
"$ref" : "#/components/schemas/Location" "enum" : [ "BROOKLYN", "BRONX", "STATTEN_ISLAND", "QUEENS" ]
}, },
"day" : { }, "day" : { },
"inventory" : { "inventory" : {
@@ -6,6 +6,8 @@ import dev.geekback.dopewars.server.routes.modelCrudRoutes
import dev.geekback.dopewars.server.routes.playerRoutes import dev.geekback.dopewars.server.routes.playerRoutes
import dev.geekback.dopewars.server.services.GameService import dev.geekback.dopewars.server.services.GameService
import dev.geekback.dopewars.server.services.MarketService import dev.geekback.dopewars.server.services.MarketService
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
@@ -13,6 +15,7 @@ import io.ktor.server.http.content.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import io.ktor.server.plugins.calllogging.* import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.plugins.openapi.* import io.ktor.server.plugins.openapi.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
@@ -27,7 +30,7 @@ fun Application.module() {
} }
println("[ # ] Loading Web Sockets Plugin") println("[ # ] Loading Web Sockets Plugin")
install(WebSockets); install(WebSockets)
println("[ # ] Loading Content Negotiation Plugin") println("[ # ] Loading Content Negotiation Plugin")
install(ContentNegotiation) { install(ContentNegotiation) {
@@ -39,6 +42,13 @@ fun Application.module() {
}) })
} }
println("[ # ] Loading CORS Plugin")
install(CORS) {
anyHost()
allowMethod(HttpMethod.Options)
allowHeader(HttpHeaders.ContentType)
}
println("[ # ] Plugins Loaded") println("[ # ] Plugins Loaded")
val marketService = MarketService() val marketService = MarketService()
@@ -52,7 +62,7 @@ fun Application.module() {
modelCrudRoutes(marketService) modelCrudRoutes(marketService)
webSocket("/ws") { webSocket("/ws") {
send("Welcome to DopeWars"); send("Welcome to DopeWars")
for (frame in incoming) { for (frame in incoming) {
if (frame is Frame.Text) send("Echo: ${frame.readText()}") if (frame is Frame.Text) send("Echo: ${frame.readText()}")
} }
@@ -24,7 +24,7 @@ fun Route.marketRoutes(
get { get {
val players = PlayerRepository.all().toList() val players = PlayerRepository.all().toList()
val response: MutableList<Market> = mutableListOf(); val response: MutableList<Market> = mutableListOf()
players.forEach { player -> players.forEach { player ->
response.add(player.lastMarket!!) response.add(player.lastMarket!!)
} }
@@ -15,10 +15,9 @@ fun Route.modelCrudRoutes(marketService: MarketService) {
// List // List
get { get {
val modelName = call.parameters["modelName"] when (val modelName = call.parameters["modelName"]) {
when (modelName) {
"market" -> { "market" -> {
call.respond(marketService.generateMarket(Location("BRONX"))) call.respond(marketService.generateMarket(Location.BRONX))
} }
else -> { else -> {
call.respond("Model name is $modelName") call.respond("Model name is $modelName")
@@ -27,7 +27,7 @@ fun Route.playerRoutes(
// Create // Create
post { post {
val location = Location("Brooklyn") val location = Location.BROOKLYN
val mkt = marketService.generateMarket(location) val mkt = marketService.generateMarket(location)
val player = Player( val player = Player(
@@ -59,7 +59,7 @@ fun Route.playerRoutes(
// Read // Read
get { get {
val players = PlayerRepository.all(); val players = PlayerRepository.all()
call.respond( call.respond(
HttpStatusCode.OK, HttpStatusCode.OK,
@@ -60,7 +60,7 @@ class GameService(
player: Player, player: Player,
drug: Drug, drug: Drug,
quantity: Int, quantity: Int,
price: Float price: Int
) { ) {
val cost = price * quantity val cost = price * quantity
@@ -90,7 +90,7 @@ class GameService(
player: Player, player: Player,
drug: Drug, drug: Drug,
quantity: Int, quantity: Int,
price: Float price: Int
) { ) {
val inventoryItem = player.inventory val inventoryItem = player.inventory
@@ -104,7 +104,7 @@ class GameService(
} }
inventoryItem.quantity -= quantity inventoryItem.quantity -= quantity
player.cash += price * quantity.toFloat() player.cash += price * quantity
if (inventoryItem.quantity == 0) { if (inventoryItem.quantity == 0) {
player.inventory.remove(inventoryItem) player.inventory.remove(inventoryItem)
@@ -8,7 +8,7 @@ import dev.geekback.dopewars.shared.Market
class MarketService { class MarketService {
fun generateMarket(location: Location): Market { fun generateMarket(location: Location): Market {
val prices: MutableMap<Drug, Float> = mutableMapOf() val prices: MutableMap<Drug, Int> = mutableMapOf()
Drugs.all.forEach { drugInfo -> Drugs.all.forEach { drugInfo ->
prices[drugInfo.drug] = drugInfo.newPrice() prices[drugInfo.drug] = drugInfo.newPrice()
@@ -20,7 +20,7 @@ import kotlinx.serialization.json.Json
* *
* Client for accessing the Dopewars API server * Client for accessing the Dopewars API server
*/ */
class ApiClient() { class ApiClient {
private var address: String = "" private var address: String = ""
private val client = HttpClient(CIO) { private val client = HttpClient(CIO) {
install(ContentNegotiation) { install(ContentNegotiation) {
@@ -17,6 +17,6 @@ enum class Drug {
companion object { companion object {
fun all() = listOf(WEED, COCAINE, HASH, ACID, HEROIN, MUSHROOMS, METH) fun all() = listOf(WEED, COCAINE, HASH, ACID, HEROIN, MUSHROOMS, METH)
fun random() = Drug.WEED fun random() = WEED
} }
} }
@@ -5,11 +5,11 @@ import kotlin.random.Random
@Serializable @Serializable
data class DrugInfo( data class DrugInfo(
val drug: Drug, var drug: Drug,
val minValue: Float, val minValue: Int,
val maxValue: Float, val maxValue: Int,
) { ) {
fun newPrice(): Float { fun newPrice(): Int {
return Random.nextFloat() * (this.maxValue - this.minValue) return Random.nextInt() * (this.maxValue - this.minValue)
} }
} }
@@ -2,13 +2,13 @@ package dev.geekback.dopewars.shared
object Drugs { object Drugs {
val all = listOf( val all = listOf(
DrugInfo(Drug.WEED, 300.0f, 1000.0f), DrugInfo(Drug.WEED, 30000, 100000),
DrugInfo(Drug.ACID, 1000.0f, 4500.0f), DrugInfo(Drug.ACID, 100000, 450000),
DrugInfo(Drug.COCAINE, 15000.0f, 30000.0f), DrugInfo(Drug.COCAINE, 1500000, 3000000),
DrugInfo(Drug.HASH, 5000.0f, 14000.0f), DrugInfo(Drug.HASH, 500000, 1400000),
DrugInfo(Drug.MUSHROOMS, 500.0f, 2500.0f), DrugInfo(Drug.MUSHROOMS, 50000, 250000),
DrugInfo(Drug.METH, 2000.0f, 9000.0f), DrugInfo(Drug.METH, 200000, 900000),
DrugInfo(Drug.HEROIN, minValue = 4000.0f, 9500.0f) DrugInfo(Drug.HEROIN, minValue = 400000, 950000)
) )
fun random() = all.random() fun random() = all.random()
@@ -3,6 +3,24 @@ package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Location( enum class Location {
var name: String BROOKLYN,
) BRONX,
STATTEN_ISLAND,
QUEENS;
companion object {
fun all(): List<Location> {
return listOf(
BROOKLYN,
BRONX,
STATTEN_ISLAND,
QUEENS
)
}
fun random(): Location {
return all().random()
}
}
}
@@ -1,13 +0,0 @@
package dev.geekback.dopewars.shared
object Locations {
val all = listOf(
Location("Brooklyn"),
Location("Bronx"),
Location("Queens"),
Location("Statten Island"),
)
fun random() = all.random()
}
@@ -4,9 +4,10 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Market( data class Market(
// Location of the market
val location: Location, val location: Location,
val prices: Map<Drug, Float> // Map of Drug to price in cents
val prices: Map<Drug, Int>
) { ) {
companion object { companion object
}
} }
@@ -2,15 +2,18 @@ package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.uuid.Uuid
@Serializable @Serializable
data class Player( data class Player(
val id: String, val id: String,
val name: String, val name: String,
var cash: Float = 2000f, // Cash in cents
var debt: Float = 5500f, var cash: Int = 200000,
// Debt in cents
var debt: Int = 550000,
var health: Int = 100, var health: Int = 100,
var location: Location = Location("Brooklyn"), var location: Location = Location.BROOKLYN,
var day: Int = 1, var day: Int = 1,
val inventory: MutableList<InventoryItem> = mutableListOf(), val inventory: MutableList<InventoryItem> = mutableListOf(),
var lastMarket: Market? = null, var lastMarket: Market? = null,
@@ -19,7 +22,7 @@ data class Player(
companion object { companion object {
fun new(): Player { fun new(): Player {
return Player( return Player(
id = "", id = Uuid.random().toString(),
name = "Default Player", name = "Default Player",
) )
} }
@@ -1,3 +1,3 @@
package dev.geekback.dopewars.shared package dev.geekback.dopewars.shared
const val DRUG_MAX_PRICE: Float = 100_000f; const val DRUG_MAX_PRICE: Float = 100_000f
@@ -5,5 +5,4 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class MarketResult( data class MarketResult(
val market: Market) { val market: Market)
}
+14 -1
View File
@@ -22,12 +22,25 @@ kotlin {
implementation(libs.compose.runtime) implementation(libs.compose.runtime)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation("org.jetbrains.compose.ui:ui-tooling-preview:1.10.0")
implementation(libs.kotlinx.datetime)
api("br.com.devsrsouza.compose.icons:feather:1.1.1")
} }
wasmJsMain.dependencies { wasmJsMain.dependencies {
implementation(libs.wrappers.browser) implementation(libs.wrappers.browser)
} }
jvmMain.dependencies {} jvmMain.dependencies {
implementation(compose.desktop.currentOs)
}
}
}
compose.desktop {
application {
mainClass = "dev.geekback.dopewars.DemoMain3Kt"
// mainClass = "dev.geekback.dopewars.ComposeTimestampControlDemoKt"
} }
} }
@@ -3,8 +3,10 @@ package dev.geekback.dopewars.ui
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -40,3 +42,46 @@ fun BorderedCard(title: String, content: @Composable () -> Unit) {
} }
} }
} }
@Composable
fun ViewFrame(
title: String,
onRequestEdit: () -> Unit,
content: @Composable () -> Unit) {
BorderedCard(title) {
Column {
content()
Row {
Button(onClick = onRequestEdit) { Text("Edit") }
}
}
}
}
@Composable
fun FormFrame(
title: String,
onModelUpdate: () -> Unit,
onModelReset: () -> Unit,
onCancelEdit: () -> Unit,
content: @Composable () -> Unit
) {
BorderedCard(title) {
Column {
content()
Row {
Button(onClick = onModelUpdate) {
Text("Update")
}
Button(onClick = onModelReset) {
Text("Reset")
}
Button(onClick = onCancelEdit) {
Text("Cancel")
}
}
}
}
}
@@ -1,13 +1,43 @@
package dev.geekback.dopewars.ui package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.ui.api.ApiUi import dev.geekback.dopewars.ui.api.ApiUi
interface DopeWarsModelUi<T> { interface DopeWarsModelUi<T> {
@Composable @Composable
fun view(model: T); fun view(model: T, onRequestEdit: () -> Unit)
@Composable @Composable
fun form(model: T, onModelUpdate: (T) -> Unit) fun form(model: T, onModelUpdate: (T) -> Unit)
@Composable
fun field(model: T, onChanged: (T) -> Unit) {
var displayModel by remember { mutableStateOf(model) }
var isEditing by remember { mutableStateOf(false) }
Column {
Text("IsEditing -> $isEditing")
if (isEditing) {
form(displayModel) {
displayModel = it
isEditing = false
onChanged(it)
}
} else {
view(displayModel) {
isEditing = true
}
}
}
}
} }
data class DopeWarsUi( data class DopeWarsUi(
@@ -1,12 +1,14 @@
package dev.geekback.dopewars.ui package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -16,7 +18,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.geekback.dopewars.shared.Locations import compose.icons.FeatherIcons
import compose.icons.feathericons.LogIn
import compose.icons.feathericons.Menu
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Player import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.TravelRequest import dev.geekback.dopewars.shared.api.TravelRequest
@@ -30,20 +35,22 @@ class TravelUi {
fun wizard(current: Player, onTicketPunched: (TravelRequest) -> Unit) { fun wizard(current: Player, onTicketPunched: (TravelRequest) -> Unit) {
var stage by remember { mutableStateOf(0) } var stage by remember { mutableStateOf(0) }
var pickedLocation by remember { mutableStateOf(0) } var pickedLocation by remember { mutableStateOf(0) }
val locations = Locations.all val locations = Location.all()
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
// Window(onCloseRequest = { onTicketPunched(TravelRequest(destination = current.location)) }, title = "Select New Location") {
/*
Card(modifier = Modifier.padding(16.dp).fillMaxSize()) { Card(modifier = Modifier.padding(16.dp).fillMaxSize()) {
Column { Column {
Text("Wizard") Text("Wizard")
var index = 0; var index = 0
when (stage) { when (stage) {
0 -> { 0 -> {
Row {
Text(text = locations[pickedLocation].name) Text(text = locations[pickedLocation].name)
IconButton(onClick = { expanded = !expanded }) { IconButton(onClick = { expanded = !expanded }) {
Icon(imageVector = FeatherIcons.Menu, contentDescription = "Login")
// Icon(MaterialIcons.Filled.Menu, contentDescription = "More options") // Icon(MaterialIcons.Filled.Menu, contentDescription = "More options")
} }
}
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false } onDismissRequest = { expanded = false }
@@ -57,7 +64,7 @@ class TravelUi {
println("location index to $index") println("location index to $index")
} }
) )
index += 1; index += 1
} }
} }
@@ -79,7 +86,5 @@ class TravelUi {
} }
} }
} }
*/
// }
} }
} }
@@ -6,7 +6,6 @@ data class ApiUi(
val playerNameUpdateRequest: PlayerNameUpdateRequestUi, val playerNameUpdateRequest: PlayerNameUpdateRequestUi,
val playerNameUpdateResponse: PlayerNameUpdateResponseUi, val playerNameUpdateResponse: PlayerNameUpdateResponseUi,
val tradeRequest: TradeRequestUi, val tradeRequest: TradeRequestUi,
// TODO: tradeResponse
val travelRequest: TravelRequestUi, val travelRequest: TravelRequestUi,
val travelResponse: TravelResponseUi, val travelResponse: TravelResponseUi,
) { ) {
@@ -16,9 +16,11 @@ import dev.geekback.dopewars.ui.LocationUi
class MarketRequestUi: DopeWarsModelUi<MarketRequest> { class MarketRequestUi: DopeWarsModelUi<MarketRequest> {
@Composable @Composable
override fun view(model: MarketRequest) { override fun view(model: MarketRequest, onRequestEdit: () -> Unit) {
BorderedCard("Market Request for ${model.location.name}") { BorderedCard("Market Request for ${model.location.name}") {
LocationUi().view(model.location) LocationUi().view(model.location) {
println("location edit requested")
}
} }
} }
@@ -45,3 +47,8 @@ class MarketRequestUi: DopeWarsModelUi<MarketRequest> {
} }
} }
} }
@Composable
fun <T>form_skeleton(model: T, onModelUpdate: (T) -> Unit, onFieldUpdate: (T) -> Unit) {
}
@@ -5,12 +5,14 @@ import dev.geekback.dopewars.shared.api.MarketResult
import dev.geekback.dopewars.ui.BorderedCard import dev.geekback.dopewars.ui.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.MarketUi import dev.geekback.dopewars.ui.MarketUi
import dev.geekback.dopewars.ui.ViewFrame
class MarketResponseUi: DopeWarsModelUi<MarketResult> { class MarketResponseUi: DopeWarsModelUi<MarketResult> {
@Composable @Composable
override fun view(model: MarketResult) { override fun view(model: MarketResult, onRequestEdit: () -> Unit) {
BorderedCard("Market Result for ${model.market.location.name}") { ViewFrame(title = "Market Result for ${model.market.location.name}",
MarketUi().view(model.market) onRequestEdit = onRequestEdit) {
println("On Request Edit")
} }
} }
@@ -25,5 +27,4 @@ class MarketResponseUi: DopeWarsModelUi<MarketResult> {
} }
} }
} }
} }
@@ -7,9 +7,9 @@ import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.ui.BorderedCard import dev.geekback.dopewars.ui.BorderedCard
class PlayerNameUpdateRequestUi(): DopeWarsModelUi<PlayerNameUpdateRequest> { class PlayerNameUpdateRequestUi : DopeWarsModelUi<PlayerNameUpdateRequest> {
@Composable @Composable
override fun view(model: PlayerNameUpdateRequest) { override fun view(model: PlayerNameUpdateRequest, onRequestEdit: () -> Unit) {
BorderedCard("Player Name Update Request for ${model.newName}") { BorderedCard("Player Name Update Request for ${model.newName}") {
} }
} }
@@ -8,9 +8,9 @@ import dev.geekback.dopewars.ui.BorderedCard
class PlayerNameUpdateResponseUi: DopeWarsModelUi<PlayerNameUpdateResponse> { class PlayerNameUpdateResponseUi: DopeWarsModelUi<PlayerNameUpdateResponse> {
@Composable @Composable
override fun view(model: PlayerNameUpdateResponse) { override fun view(model: PlayerNameUpdateResponse, onRequestEdit: () -> Unit) {
BorderedCard("Player Name Update Response") { BorderedCard("Player Name Update Response") {
PlayerUi().view(model.updatedPlayer) PlayerUi().view(model.updatedPlayer) {}
} }
} }
@@ -14,7 +14,7 @@ import dev.geekback.dopewars.ui.DopeWarsModelUi
class TradeRequestUi: DopeWarsModelUi<TradeRequest> { class TradeRequestUi: DopeWarsModelUi<TradeRequest> {
@Composable @Composable
override fun view(model: TradeRequest) { override fun view(model: TradeRequest, onRequestEdit: () -> Unit) {
BorderedCard("Trade Request for ${model.drug.name} / ${model.action} / ${model.quantity}") { BorderedCard("Trade Request for ${model.drug.name} / ${model.action} / ${model.quantity}") {
} }
@@ -26,9 +26,6 @@ class TradeRequestUi: DopeWarsModelUi<TradeRequest> {
onModelUpdate: (TradeRequest) -> Unit onModelUpdate: (TradeRequest) -> Unit
) { ) {
var localModel by remember { mutableStateOf(model) } var localModel by remember { mutableStateOf(model) }
var actionIndex by remember { mutableStateOf(0) }
var quantity by remember { mutableStateOf(model.quantity) }
var drug by remember { mutableStateOf(model.drug) }
BorderedCard("Trade Requesst Form") { BorderedCard("Trade Requesst Form") {
Row { Row {
@@ -8,7 +8,7 @@ import dev.geekback.dopewars.ui.BorderedCard
class TravelRequestUi: DopeWarsModelUi<TravelRequest> { class TravelRequestUi: DopeWarsModelUi<TravelRequest> {
@Composable @Composable
override fun view(model: TravelRequest) { override fun view(model: TravelRequest, onRequestEdit: () -> Unit) {
BorderedCard("Travel Request to ${model.destination.name}") { BorderedCard("Travel Request to ${model.destination.name}") {
} }
} }
@@ -17,27 +17,24 @@ import dev.geekback.dopewars.ui.MarketUi
class TravelResponseUi: DopeWarsModelUi<TravelResult> { class TravelResponseUi: DopeWarsModelUi<TravelResult> {
@Composable @Composable
override fun view(model: TravelResult) { override fun view(model: TravelResult, onRequestEdit: () -> Unit) {
BorderedCard("Travel Result ${model.location.name}") { BorderedCard("Travel Result ${model.location.name}") {
LocationUi().view(model.location) LocationUi().view(model.location) {}
Text("Day: ${model.day}") Text("Day: ${model.day}")
MarketUi().view(model.market) MarketUi().view(model.market) {}
} }
} }
@Composable @Composable
override fun form(model: TravelResult, onModelUpdate: (TravelResult) -> Unit) { override fun form(model: TravelResult, onModelUpdate: (TravelResult) -> Unit) {
var day by remember { mutableStateOf(model.day) }
var location by remember { mutableStateOf(model.location) }
var market by remember { mutableStateOf(model.market) }
var localModel by remember { mutableStateOf(model) }
BorderedCard("Travel Result Form") { BorderedCard("Travel Result Form") {
LocationUi().form(model.location) { LocationUi().form(model.location) {
println("Update location to $it") println("Update location to $it")
} }
// //
// //
// Row { // Row {
@@ -54,3 +51,21 @@ class TravelResponseUi: DopeWarsModelUi<TravelResult> {
} }
} }
@Composable
fun ViewFormButtons(onRequestEdit: () -> Unit) {
Row {
Button(onClick = onRequestEdit) {
Text("Edit")
}
}
}
@Composable
fun FormButtons(onModelUpdate: () -> Unit, onModelReset: () -> Unit) {
Row {
Button(onClick = onModelUpdate) { Text("Apply") }
Button(onClick = onModelReset) { Text("Reset") }
}
}
@@ -1,92 +1,110 @@
package dev.geekback.dopewars.ui package dev.geekback.dopewars.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.onClick
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.RangeSlider import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import dev.geekback.dopewars.shared.DRUG_MAX_PRICE import dev.geekback.dopewars.shared.DRUG_MAX_PRICE
import dev.geekback.dopewars.shared.Drug import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo import dev.geekback.dopewars.shared.DrugInfo
class DrugInfoUi: DopeWarsModelUi<DrugInfo> { class DrugInfoUi : DopeWarsModelUi<DrugInfo> {
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
override fun view(model: DrugInfo) { fun picker(default: Drug, onSelect: (Drug) -> Unit) {
BorderedCard(model.drug.name) { var showOptions by remember { mutableStateOf(false) }
Column { Column {
Text("Price: ${model.minValue} - ${model.maxValue}") Row {
Text(
modifier = Modifier.onClick {
showOptions = true
},
text = default.name
)
DropdownMenu(
expanded = showOptions,
onDismissRequest = { showOptions = false },
) {
Drug.all().forEach { localDrug ->
DropdownMenuItem(
onClick = {
showOptions = false
onSelect(localDrug)
},
text = { Text(localDrug.name) }
)
} }
} }
} }
}
}
@Composable
override fun view(model: DrugInfo, onRequestEdit: () -> Unit) {
ViewFrame(title = model.drug.name, onRequestEdit = onRequestEdit) {
Text("Price: ${model.minValue} - ${model.maxValue}")
}
}
@Composable @Composable
override fun form( override fun form(
model: DrugInfo, model: DrugInfo,
onModelUpdate: (DrugInfo) -> Unit onModelUpdate: (DrugInfo) -> Unit
) { ) {
var drug by remember { mutableStateOf(model) } var drug by remember { mutableStateOf(model.drug) }
var name by remember { mutableStateOf(model.drug.name) } var minPrice by remember { mutableIntStateOf(model.minValue) }
var minPrice by remember { mutableFloatStateOf(model.minValue) } var maxPrice by remember { mutableIntStateOf(model.maxValue) }
var maxPrice by remember { mutableFloatStateOf(model.maxValue) }
BorderedCard("Drug Form") { FormFrame(
Column { title = "Drug Form",
// Name onModelUpdate = { onModelUpdate(DrugInfo(drug, minPrice, maxPrice)) },
TextField( onModelReset = {
value = name, drug = model.drug
onValueChange = { name = it }, minPrice = model.minValue
label = { Text("Name") }, maxPrice = model.maxValue
) },
onCancelEdit = { onModelUpdate(model) }) {
picker(drug) {
drug = it
}
Row { Row {
TextField( TextField(
value = minPrice.toString(), value = minPrice.toString(),
onValueChange = { minPrice = it.toFloatOrNull() ?: minPrice }, onValueChange = { minPrice = it.toIntOrNull() ?: minPrice },
label = { Text("Min Price") }, label = { Text("Min Price") },
) )
TextField( TextField(
value = maxPrice.toString(), value = maxPrice.toString(),
onValueChange = { maxPrice = it.toFloatOrNull() ?: maxPrice }, onValueChange = { maxPrice = it.toIntOrNull() ?: maxPrice },
label = { Text("Max Price") }, label = { Text("Max Price") },
) )
} }
RangeSlider( RangeSlider(
value = (minPrice)..(maxPrice), value = (minPrice.toFloat())..(maxPrice.toFloat()),
onValueChangeFinished = { }, onValueChangeFinished = { },
valueRange = 1f..DRUG_MAX_PRICE, valueRange = 1f..DRUG_MAX_PRICE,
onValueChange = { it -> onValueChange = {
minPrice = it.start minPrice = it.start.toInt()
maxPrice = it.endInclusive maxPrice = it.endInclusive.toInt()
} }
) )
Row {
Button(onClick = {
drug = DrugInfo(Drug.WEED , minPrice, maxPrice)
onModelUpdate(drug)
}) {
Text("Apply")
}
Button(onClick = {
name = Drug.WEED.name
minPrice = drug.minValue
maxPrice = drug.maxValue
}) {
Text("Cancel")
}
}
}
}
} }
} }
}
@@ -1,5 +1,6 @@
package dev.geekback.dopewars.ui package dev.geekback.dopewars.ui
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -7,9 +8,17 @@ import dev.geekback.dopewars.shared.InventoryItem
class InventoryUi: DopeWarsModelUi<List<InventoryItem>> { class InventoryUi: DopeWarsModelUi<List<InventoryItem>> {
@Composable @Composable
override fun view(model: List<InventoryItem>) { override fun view(model: List<InventoryItem>, onRequestEdit: () -> Unit) {
BorderedCard("Inventory Items") { ViewFrame("Inventory",
onRequestEdit = onRequestEdit) {
LazyColumn {
items(model.size) { index ->
val item = model[index]
InventoryItemUi().view(item) {
println("Item Edit Request")
}
}
}
} }
} }
@@ -24,5 +33,4 @@ class InventoryUi: DopeWarsModelUi<List<InventoryItem>> {
} }
} }
} }
} }
@@ -1,12 +1,28 @@
package dev.geekback.dopewars.ui package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.shared.InventoryItem import dev.geekback.dopewars.shared.InventoryItem
class InventoryItemUi: DopeWarsModelUi<InventoryItem> { class InventoryItemUi: DopeWarsModelUi<InventoryItem> {
@Composable private val drugInfoUi = DrugInfoUi()
override fun view(model: InventoryItem) {
@Composable
override fun view(model: InventoryItem, onRequestEdit: () -> Unit) {
BorderedCard("") {
Text("${model.drug.name} / ${model.quantity}g")
Button(onClick = onRequestEdit) {
Text("Edit")
}
}
} }
@Composable @Composable
@@ -14,6 +30,34 @@ class InventoryItemUi: DopeWarsModelUi<InventoryItem> {
model: InventoryItem, model: InventoryItem,
onModelUpdate: (InventoryItem) -> Unit onModelUpdate: (InventoryItem) -> Unit
) { ) {
TODO("Not yet implemented") var drug by remember { mutableStateOf(model.drug) }
var quantity by remember { mutableStateOf(model.quantity) }
ViewFrame("Inventory Item",
onRequestEdit = {}) {
drugInfoUi.picker(drug) {
drug = it
}
TextField(
value = quantity.toString(),
label = { Text("Quantity in g") },
onValueChange = {
quantity = it.toInt()
})
Row {
Button(onClick = { onModelUpdate(InventoryItem(drug, quantity)) }) {
Text("Apply")
}
Button(onClick = {
drug = model.drug
quantity = model.quantity
} ) {
Text("Reset")
}
}
}
} }
} }
@@ -1,53 +1,78 @@
package dev.geekback.dopewars.ui package dev.geekback.dopewars.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button import androidx.compose.foundation.onClick
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import dev.geekback.dopewars.shared.Location import dev.geekback.dopewars.shared.Location
class LocationUi: DopeWarsModelUi<Location> { class LocationUi : DopeWarsModelUi<Location> {
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
override fun view(model: Location) { fun picker(default: Location, onSelect: (Location) -> Unit) {
BorderedCard("Location - ${model.name}") { var showOptions by remember { mutableStateOf(false) }
Column {
Row {
Text(
text = default.name,
modifier = Modifier.onClick { showOptions = true }
)
DropdownMenu(
expanded = showOptions,
onDismissRequest = { showOptions = false }
) {
Location.all().forEach { localLocation ->
DropdownMenuItem(onClick = {
showOptions = false
onSelect(localLocation)
}, text = { Text(localLocation.name) })
}
}
}
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @Composable
override fun view(model: Location, onRequestEdit: () -> Unit) {
ViewFrame(title = "Location",
onRequestEdit = onRequestEdit) {
Text(model.name)
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
override fun form( override fun form(
model: Location, model: Location,
onModelUpdate: (Location) -> Unit onModelUpdate: (Location) -> Unit
) { ) {
var name by remember { mutableStateOf(model.name) } var localModel by remember { mutableStateOf(model) }
var location by remember { mutableStateOf(model)}
BorderedCard("Location") { val title = if (localModel == model) { "Location" } else { "Location *" }
TextField(value = name, label = {
Text("Location Name")
}, onValueChange = {
name = it
})
Row { FormFrame(title = title,
Button(onClick = { onModelUpdate = {
location.name = name; onModelUpdate(localModel)
onModelUpdate(location) },
onModelReset = {
localModel = model
},
onCancelEdit = {
onModelUpdate(model)
}) { }) {
Text("Apply") picker(localModel) {
} localModel = it
Button(onClick = {
name = location.name
}) {
Text("Cancel")
}
} }
} }
} }
@@ -2,24 +2,34 @@ package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.shared.Market import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
class MarketUi: DopeWarsModelUi<Market> { class MarketUi: DopeWarsModelUi<Market> {
@Composable @Composable
override fun view(model: Market) { override fun view(model: Market, onRequestEdit: () -> Unit) {
val prices = model.prices.toList() val prices = model.prices.toList()
BorderedCard("Market for ${model.location.name}") { BorderedCard("Market for ${model.location.name}") {
Column { Column {
LazyColumn { LazyColumn {
items(prices.size) { index -> items(prices.size) { index ->
val item = prices[index];
val item = prices[index]
val name = item.first.name val name = item.first.name
val price = item.second.toFloat() val price = item.second
Text("Drug $name $${price}") Text("Drug $name $${price}")
} }
} }
Button(onClick = onRequestEdit) {
Text("Edit")
}
} }
} }
} }
@@ -1,44 +1,99 @@
package dev.geekback.dopewars.ui package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.material3.TextField
import androidx.compose.runtime.*
import dev.geekback.dopewars.shared.Player import dev.geekback.dopewars.shared.Player
import kotlin.time.Instant import kotlin.time.Instant
class PlayerUi: DopeWarsModelUi<Player> { class PlayerUi : DopeWarsModelUi<Player> {
private val inventoryUi = InventoryUi()
private val marketUi = MarketUi()
private val locationUi = LocationUi()
@Composable @Composable
override fun view(model: Player) { override fun view(model: Player, onRequestEdit: () -> Unit) {
BorderedCard(model.name) { ViewFrame(
Column { title = model.name,
onRequestEdit = onRequestEdit
) {
Text("ID: ${model.id}") Text("ID: ${model.id}")
Text("Cash: $${model.cash}") Text("Cash: $${model.cash}")
Text("Debt: $${model.debt}") Text("Debt: $${model.debt}")
Text("Health: ${model.health}") Text("Health: ${model.health}")
Text("Location: ${model.location.name}") Text("Location: ${model.location.name}")
// TODO: this is an ugly memory hack this.inventoryUi.view(model.inventory) {
InventoryUi().view(model.inventory) println("Inventory Edit request")
}
// marketView(model.lastMarket ?: Market(location = Location("Unknown"), prices = mapOf())) // marketView(model.lastMarket ?: Market(location = Location("Unknown"), prices = mapOf()))
val lastTime = Instant.fromEpochSeconds(model.lastActionTime) val lastTime = Instant.fromEpochSeconds(model.lastActionTime)
Text("Last Action: $lastTime") Text("Last Action: $lastTime")
} }
} }
}
@Composable @Composable
override fun form( override fun form(
model: Player, model: Player,
onModelUpdate: (Player) -> Unit onModelUpdate: (Player) -> Unit
) { ) {
BorderedCard("Player ${model.name}") { var uuid by remember { mutableStateOf(model.id) }
Button(onClick = { onModelUpdate(model) }) { var name by remember { mutableStateOf(model.name) }
Text("Update") var day by remember { mutableStateOf(model.day) }
} var cash by remember { mutableStateOf(model.cash) }
} var debt by remember { mutableStateOf(model.debt) }
} var health by remember { mutableStateOf(model.health) }
} var location by remember { mutableStateOf(model.location) }
var inventory by remember { mutableStateOf(model.inventory) }
var lastActionTime by remember { mutableStateOf(model.lastActionTime) }
var lastMarket by remember { mutableStateOf(model.lastMarket) }
@Composable FormFrame(
fun playerView(player: Player) { title = "Player ${model.name} Form",
onModelUpdate = {
onModelUpdate(
Player(
id = uuid,
cash = cash,
debt = debt,
health = health,
location = location,
inventory = inventory,
name = name,
day = day,
lastMarket = lastMarket,
lastActionTime = lastActionTime,
)
)
},
onModelReset = {
uuid = model.id
name = model.name
day = model.day
cash = model.cash
debt = model.debt
health = model.health
location = model.location
inventory = model.inventory
lastActionTime = model.lastActionTime
lastMarket = model.lastMarket
},
onCancelEdit = {
onModelUpdate(model)
}) {
TextField(value = uuid, onValueChange = { uuid = it })
TextField(value = name, onValueChange = { name = it })
TextField(value = day.toString(), onValueChange = { day = it.toInt() })
TextField(value = cash.toString(), onValueChange = { cash = it.toInt() })
TextField(value = debt.toString(), onValueChange = { debt = it.toInt() })
TextField(value = health.toString(), onValueChange = { health = it.toInt() })
this.locationUi.picker(location) { location = it }
TextField(value = lastActionTime.toString(), onValueChange = { lastActionTime = it.toLong() })
if (lastMarket == null) {
Text("No Market")
} else {
this.marketUi.view(lastMarket!!) { }
}
}
}
} }
@@ -0,0 +1,82 @@
package dev.geekback.util
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.onClick
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Instant
data class ComposeTimestampConfiguration(
val minimumDate: Long,
val maximumDate: Long,
)
@Composable
@Preview
fun ComposableTimestampControlPreview() {
Box {
ComposeTimestampControl(
initialValue = 123456789,
configuration = ComposeTimestampConfiguration(minimumDate = 0, maximumDate = 1234567890),
onValueUpdated = { println("Updated to $it") },
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ComposeTimestampControl(initialValue: Long, configuration: ComposeTimestampConfiguration, onValueUpdated: (Long) -> Unit) {
var currentValue by remember { mutableStateOf(initialValue) }
val currentDate = Instant.fromEpochSeconds(currentValue).toLocalDateTime(TimeZone.currentSystemDefault()).date
val currentTime = Instant.fromEpochSeconds(currentValue).toLocalDateTime(TimeZone.currentSystemDefault()).time
var showingDatePicker by remember { mutableStateOf(false) }
var showingTimePicker by remember { mutableStateOf(false) }
var datePickerState = rememberDatePickerState(
initialSelectedDateMillis = initialValue,
)
Column {
Row {
Text("$currentDate", modifier = Modifier.onClick(enabled = true, onClick = {
showingDatePicker = true
}))
Text("$currentTime", modifier = Modifier.onClick(enabled = true, onClick = {
showingTimePicker = true
}))
}
Row {
Text("Date Visible $showingDatePicker")
Text("Time Visible $showingTimePicker")
}
HorizontalDivider()
if (showingDatePicker) {
DatePicker(
state = datePickerState
)
}
if (showingTimePicker) {
}
// Controls
Row {
Button(onClick = { currentValue = initialValue }) { Text("Reset") }
Button(onClick = { onValueUpdated(currentValue) }) { Text("Set") }
Button(onClick = { onValueUpdated(initialValue + currentValue) }) { Text("Cancel") }
}
}
}
@@ -0,0 +1,33 @@
package dev.geekback.dopewars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import dev.geekback.util.ComposeTimestampConfiguration
import dev.geekback.util.ComposeTimestampControl
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "ComposeTimestampControlDemoApp"
) {
ComposeTimestampControlDemoApp()
}
}
@Composable
fun ComposeTimestampControlDemoApp() {
var currentValue by remember { mutableStateOf(1234567890L )}
ComposeTimestampControl(
initialValue = currentValue,
configuration = ComposeTimestampConfiguration(minimumDate = 0, maximumDate = 12345678900),
) {
println("Updated from $currentValue to $it")
currentValue = it
}
}
@@ -0,0 +1,224 @@
package dev.geekback.dopewars
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo
import dev.geekback.dopewars.shared.Location
import dev.geekback.dopewars.shared.Market
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.MarketRequest
import dev.geekback.dopewars.shared.api.MarketResult
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.shared.api.PlayerNameUpdateResponse
import dev.geekback.dopewars.shared.api.TradeAction
import dev.geekback.dopewars.shared.api.TradeRequest
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.shared.api.TravelResult
import dev.geekback.dopewars.ui.DopeWarsUi
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "DopeWars Dev Client"
) {
DemoApp()
}
}
data class ApiData(
var marketRequest: MarketRequest,
var marketResult: MarketResult,
var playerNameUpdateRequest: PlayerNameUpdateRequest,
var playerNameUpdateResponse: PlayerNameUpdateResponse,
var tradeAction: TradeAction,
var tradeRequest: TradeRequest,
var travelRequest: TravelRequest,
var travelResult: TravelResult
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DemoApp() {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
var currentScreen by remember { mutableStateOf(Screens.HOME) }
var drug by remember { mutableStateOf(DrugInfo(drug = Drug.random(), minValue = 10000, maxValue = 100000)) }
var player by remember { mutableStateOf(Player.new()) }
var location by remember { mutableStateOf(Location.random()) }
var market by remember {
mutableStateOf(
Market(
location = Location.random(),
prices = mapOf()
)
)
}
val ui = DopeWarsUi.new()
val travelResultLocation = Location.random()
var api by remember {
mutableStateOf(
ApiData(
marketRequest = MarketRequest(
location = Location.random()
),
marketResult = MarketResult(
market = Market(
location = Location.random(),
prices = emptyMap()
)
),
playerNameUpdateRequest = PlayerNameUpdateRequest("newPlayerName"),
playerNameUpdateResponse = PlayerNameUpdateResponse(
updatedPlayer = Player.new()
),
tradeAction = TradeAction.BUY,
tradeRequest = TradeRequest(
action = TradeAction.BUY,
drug = Drug.random(),
quantity = 10
),
travelRequest = TravelRequest(
destination = Location.random()
),
travelResult = TravelResult(
location = travelResultLocation,
day = 1,
market = Market(location = travelResultLocation, mapOf())
)
)
)
}
MaterialTheme {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
val buttons = listOf(
Pair(Screens.HOME, "Home"),
Pair(Screens.DRUG, "Drugs"),
Pair(Screens.PLAYER, "Player"),
Pair(Screens.LOCATION, "Location"),
Pair(Screens.API, "API")
)
buttons.forEach { pair ->
Button(onClick = {
currentScreen = pair.first
}) {
Text(pair.second)
}
}
}
}
) {
Column { // Main content
when (currentScreen) {
Screens.HOME -> {
Row {
Box(modifier = Modifier.width(400.dp)) {
ui.player.field(player) {
println("Player changed -> $it")
}
}
ui.location.field(location) {
println("Location changed -> $it")
}
}
}
Screens.DRUG -> {
ui.drug.field(drug) {
println("Drug Updated -> $it")
}
}
Screens.PLAYER -> {
ui.player.form(player) {
player = it
}
}
Screens.LOCATION -> {
ui.location.form(location) {
println("Location updated from $location to $it")
location = it
}
}
Screens.MARKET -> {
ui.market.form(market) {
market = it
}
}
Screens.API -> {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
SizeBox(250.dp, 250.dp) {
ui.api.marketRequest.form(api.marketRequest) { api.marketRequest = it }
}
SizeBox(250.dp, 250.dp) {
ui.api.marketResponse.form(api.marketResult) { api.marketResult = it }
}
}
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
SizeBox(250.dp, 250.dp) {
ui.api.playerNameUpdateRequest.form(api.playerNameUpdateRequest) {
api.playerNameUpdateRequest = it
}
}
SizeBox(250.dp, 250.dp) {
ui.api.playerNameUpdateResponse.form(api.playerNameUpdateResponse) {
api.playerNameUpdateResponse = it
}
}
}
ui.api.travelRequest.form(api.travelRequest) { api.travelRequest = it }
ui.api.travelResponse.form(api.travelResult) { api.travelResult = it }
ui.api.tradeRequest.form(api.tradeRequest) { api.tradeRequest = it }
}
}
}
}
}
}
@Composable
fun SizeBox(width: Dp, height: Dp, body: @Composable () -> Unit) {
Box(modifier = Modifier.width(width).height(height)) {
body()
}
}
@@ -1,4 +1,4 @@
package dev.geekback.dopewars.desktop package dev.geekback.dopewars
enum class Screens { enum class Screens {
HOME, HOME,
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KotlinProject</title>
<link type="text/css" rel="stylesheet" href="styles.css">
</head>
<body style="text-align: center; align-content: center">
<h1>Web Client HTML</h1>
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 50 50" role="presentation">
<circle cx="25" cy="25" r="20" stroke="#ccc" stroke-width="4" fill="none"/>
<circle cx="25" cy="25" r="20" stroke="#333" stroke-width="4" fill="none" stroke-linecap="round"
stroke-dasharray="90 125">
<animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s"
repeatCount="indefinite"/>
</circle>
</svg>
<script type="application/javascript" src="web.js"></script>
</body>
</html>
@@ -1,7 +1,10 @@
package dev.geekback.dopewars.web package dev.geekback.dopewars.web
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -15,6 +18,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.geekback.dopewars.shared.Player import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.ui.PlayerUi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -22,14 +26,15 @@ import kotlinx.coroutines.launch
fun App() { fun App() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var players by mutableStateOf (listOf<Player>()) var players by mutableStateOf (listOf<Player>())
var activePlayer: Player? by mutableStateOf(null)
MaterialTheme { MaterialTheme {
Scaffold( Scaffold(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
content = { content = {
Column { Row {
Column(modifier = Modifier.width(250.dp)) {
// List of current Players // List of current Players
Button(onClick = { Button(onClick = {
println("Reload Players Now")
scope.launch { scope.launch {
players = AdminApi.loadPlayers() players = AdminApi.loadPlayers()
} }
@@ -38,16 +43,26 @@ fun App() {
} }
ApiControlBox { ApiControlBox {
println("Setting API BaseUrl to $it")
AdminApi.setBaseUrl(it) AdminApi.setBaseUrl(it)
} }
PlayersList(players) { PlayersList(players) {
println("Click on $it") activePlayer = it
}
}
Column(modifier = Modifier.fillMaxSize()) {
if (players.isEmpty()) {
Text("No Players")
} else {
if (activePlayer != null) {
PlayerUi().view(activePlayer!!) {}
} else {
Text("Select a Player")
}
}
} }
} }
} }
) )
} }
} }
@@ -19,7 +19,7 @@ fun PlayersList(players: List<Player>, onPLayerSelected: (Player) -> Unit) {
} }
} else { } else {
items(players.size) { i -> items(players.size) { i ->
val item = players[i]; val item = players[i]
Text("Player ${item.name}", modifier = Modifier.clickable { onPLayerSelected(item) }) Text("Player ${item.name}", modifier = Modifier.clickable { onPLayerSelected(item) })
} }
} }