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 {
kotlin("jvm")
// alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.compose)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
@@ -9,8 +8,6 @@ plugins {
dependencies {
implementation(project(":shared"))
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.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.core)
@@ -9,6 +9,7 @@ import dev.geekback.dopewars.shared.api.TravelRequest
class ClientState(player: Player) {
var player by mutableStateOf(player)
var hasLoggedIn by mutableStateOf(false)
var apiUrl: String = "http://localhost:8080"
private var api: ApiClient = ApiClient()
@@ -21,9 +22,15 @@ class ClientState(player: Player) {
suspend fun login() : Player {
val p = this.api.login("http://localhost:8080")
println("Logged in as ${p.name} / ${p.id}")
this.hasLoggedIn = true
return p
}
suspend fun logout() {
this.hasLoggedIn = false
}
suspend fun updateMarket() {
println("Updating market")
this.player.lastMarket = this.api.market(this.player)
@@ -37,8 +44,7 @@ class ClientState(player: Player) {
}
fun isLoggedIn(): Boolean {
println("Testing if user is logged in - ${this.player.id.isNotEmpty()}")
return this.player.id.isNotEmpty()
return this.hasLoggedIn
}
fun refreshApi(url: String) {
@@ -48,7 +54,7 @@ class ClientState(player: Player) {
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.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -21,6 +22,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
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.PlayerUi
import dev.geekback.dopewars.ui.TravelUi
@@ -51,31 +54,45 @@ fun App() {
ModalDrawerSheet {
if (state.isLoggedIn()) {
// Display the current player state
PlayerUi().view(state.player)
PlayerUi().view(state.player) {}
} else {
NavigationDrawerItem(
label = {
Row {
/*
Icon(
modifier = Modifier.padding(16.dp),
imageVector = ,
contentDescription = "Login Icon"
)
*/
Text(
modifier = Modifier.padding(16.dp),
text = "Login"
)
if (state.isLoggedIn()) {
NavigationDrawerItem(
label = {
Row {
Text("Logout")
}
},
selected = false,
onClick = {
scope.launch {
state.logout()
}
}
},
selected = false,
onClick = {
scope.launch {
state.player = state.login()
)
} else {
NavigationDrawerItem(
label = {
Row {
Icon(
modifier = Modifier.padding(16.dp),
imageVector = FeatherIcons.LogIn,
contentDescription = "Login Icon"
)
Text(
modifier = Modifier.padding(16.dp),
text = "Login"
)
}
},
selected = false,
onClick = {
scope.launch {
state.player = state.login()
}
}
}
)
)
}
}
}
}
@@ -91,13 +108,12 @@ fun App() {
}
}
@Composable
fun mainWindow(state: ClientState) {
val scope = rememberCoroutineScope();
val scope = rememberCoroutineScope()
var showingTravelWizard by remember { mutableStateOf(false) }
PlayerUi().view(state.player)
PlayerUi().view(state.player) {}
Row {
Button(onClick = {
println("Transactions")
+3
View File
@@ -1,2 +1,5 @@
org.gradle.configuration-cache=true
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]
ktor = "3.5.1"
ktor = "3.5.2"
kotlin = "2.4.0"
compose = "1.11.1"
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-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-cors = { group = "io.ktor", name="ktor-server-cors", version.ref="ktor" }
# KTOR client
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" }
+3
View File
@@ -46,6 +46,9 @@ dependencies {
// Websockets
implementation(libs.ktor.server.websockets)
// CORS
implementation(libs.ktor.server.cors)
// General Use
implementation(libs.kotlinx.datetime)
+2 -9
View File
@@ -698,20 +698,13 @@
},
"quantity" : { }
}
};
defs["Location"] = {
"title" : "Location",
"required" : [ "name" ],
"properties" : {
"name" : { }
}
};
defs["Market"] = {
"title" : "Market",
"required" : [ "location", "prices" ],
"properties" : {
"location" : {
"$ref" : "#/components/schemas/Location"
"enum" : [ "BROOKLYN", "BRONX", "STATTEN_ISLAND", "QUEENS" ]
},
"prices" : {
"additionalProperties" : { }
@@ -737,7 +730,7 @@
"debt" : { },
"health" : { },
"location" : {
"$ref" : "#/components/schemas/Location"
"enum" : [ "BROOKLYN", "BRONX", "STATTEN_ISLAND", "QUEENS" ]
},
"day" : { },
"inventory" : {
@@ -6,6 +6,8 @@ import dev.geekback.dopewars.server.routes.modelCrudRoutes
import dev.geekback.dopewars.server.routes.playerRoutes
import dev.geekback.dopewars.server.services.GameService
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.server.application.*
import io.ktor.server.engine.*
@@ -13,6 +15,7 @@ import io.ktor.server.http.content.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.plugins.openapi.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
@@ -27,7 +30,7 @@ fun Application.module() {
}
println("[ # ] Loading Web Sockets Plugin")
install(WebSockets);
install(WebSockets)
println("[ # ] Loading Content Negotiation Plugin")
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")
val marketService = MarketService()
@@ -52,7 +62,7 @@ fun Application.module() {
modelCrudRoutes(marketService)
webSocket("/ws") {
send("Welcome to DopeWars");
send("Welcome to DopeWars")
for (frame in incoming) {
if (frame is Frame.Text) send("Echo: ${frame.readText()}")
}
@@ -24,7 +24,7 @@ fun Route.marketRoutes(
get {
val players = PlayerRepository.all().toList()
val response: MutableList<Market> = mutableListOf();
val response: MutableList<Market> = mutableListOf()
players.forEach { player ->
response.add(player.lastMarket!!)
}
@@ -15,10 +15,9 @@ fun Route.modelCrudRoutes(marketService: MarketService) {
// List
get {
val modelName = call.parameters["modelName"]
when (modelName) {
when (val modelName = call.parameters["modelName"]) {
"market" -> {
call.respond(marketService.generateMarket(Location("BRONX")))
call.respond(marketService.generateMarket(Location.BRONX))
}
else -> {
call.respond("Model name is $modelName")
@@ -27,7 +27,7 @@ fun Route.playerRoutes(
// Create
post {
val location = Location("Brooklyn")
val location = Location.BROOKLYN
val mkt = marketService.generateMarket(location)
val player = Player(
@@ -59,7 +59,7 @@ fun Route.playerRoutes(
// Read
get {
val players = PlayerRepository.all();
val players = PlayerRepository.all()
call.respond(
HttpStatusCode.OK,
@@ -60,7 +60,7 @@ class GameService(
player: Player,
drug: Drug,
quantity: Int,
price: Float
price: Int
) {
val cost = price * quantity
@@ -90,7 +90,7 @@ class GameService(
player: Player,
drug: Drug,
quantity: Int,
price: Float
price: Int
) {
val inventoryItem = player.inventory
@@ -104,7 +104,7 @@ class GameService(
}
inventoryItem.quantity -= quantity
player.cash += price * quantity.toFloat()
player.cash += price * quantity
if (inventoryItem.quantity == 0) {
player.inventory.remove(inventoryItem)
@@ -8,7 +8,7 @@ import dev.geekback.dopewars.shared.Market
class MarketService {
fun generateMarket(location: Location): Market {
val prices: MutableMap<Drug, Float> = mutableMapOf()
val prices: MutableMap<Drug, Int> = mutableMapOf()
Drugs.all.forEach { drugInfo ->
prices[drugInfo.drug] = drugInfo.newPrice()
@@ -20,7 +20,7 @@ import kotlinx.serialization.json.Json
*
* Client for accessing the Dopewars API server
*/
class ApiClient() {
class ApiClient {
private var address: String = ""
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
@@ -17,6 +17,6 @@ enum class Drug {
companion object {
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
data class DrugInfo(
val drug: Drug,
val minValue: Float,
val maxValue: Float,
var drug: Drug,
val minValue: Int,
val maxValue: Int,
) {
fun newPrice(): Float {
return Random.nextFloat() * (this.maxValue - this.minValue)
fun newPrice(): Int {
return Random.nextInt() * (this.maxValue - this.minValue)
}
}
@@ -2,13 +2,13 @@ package dev.geekback.dopewars.shared
object Drugs {
val all = listOf(
DrugInfo(Drug.WEED, 300.0f, 1000.0f),
DrugInfo(Drug.ACID, 1000.0f, 4500.0f),
DrugInfo(Drug.COCAINE, 15000.0f, 30000.0f),
DrugInfo(Drug.HASH, 5000.0f, 14000.0f),
DrugInfo(Drug.MUSHROOMS, 500.0f, 2500.0f),
DrugInfo(Drug.METH, 2000.0f, 9000.0f),
DrugInfo(Drug.HEROIN, minValue = 4000.0f, 9500.0f)
DrugInfo(Drug.WEED, 30000, 100000),
DrugInfo(Drug.ACID, 100000, 450000),
DrugInfo(Drug.COCAINE, 1500000, 3000000),
DrugInfo(Drug.HASH, 500000, 1400000),
DrugInfo(Drug.MUSHROOMS, 50000, 250000),
DrugInfo(Drug.METH, 200000, 900000),
DrugInfo(Drug.HEROIN, minValue = 400000, 950000)
)
fun random() = all.random()
@@ -3,6 +3,24 @@ package dev.geekback.dopewars.shared
import kotlinx.serialization.Serializable
@Serializable
data class Location(
var name: String
)
enum class Location {
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
data class Market(
// Location of the market
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 kotlin.time.Clock
import kotlin.uuid.Uuid
@Serializable
data class Player(
val id: String,
val name: String,
var cash: Float = 2000f,
var debt: Float = 5500f,
// Cash in cents
var cash: Int = 200000,
// Debt in cents
var debt: Int = 550000,
var health: Int = 100,
var location: Location = Location("Brooklyn"),
var location: Location = Location.BROOKLYN,
var day: Int = 1,
val inventory: MutableList<InventoryItem> = mutableListOf(),
var lastMarket: Market? = null,
@@ -19,7 +22,7 @@ data class Player(
companion object {
fun new(): Player {
return Player(
id = "",
id = Uuid.random().toString(),
name = "Default Player",
)
}
@@ -1,3 +1,3 @@
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
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.foundation)
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 {
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.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
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
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
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
interface DopeWarsModelUi<T> {
@Composable
fun view(model: T);
fun view(model: T, onRequestEdit: () -> Unit)
@Composable
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(
@@ -1,12 +1,14 @@
package dev.geekback.dopewars.ui
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.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -16,7 +18,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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.api.TravelRequest
@@ -30,19 +35,21 @@ class TravelUi {
fun wizard(current: Player, onTicketPunched: (TravelRequest) -> Unit) {
var stage by remember { mutableStateOf(0) }
var pickedLocation by remember { mutableStateOf(0) }
val locations = Locations.all
val locations = Location.all()
var expanded by remember { mutableStateOf(false) }
// Window(onCloseRequest = { onTicketPunched(TravelRequest(destination = current.location)) }, title = "Select New Location") {
/*
Card(modifier = Modifier.padding(16.dp).fillMaxSize()) {
Column {
Text("Wizard")
var index = 0;
var index = 0
when (stage) {
0 -> {
Text(text = locations[pickedLocation].name)
IconButton(onClick = { expanded = !expanded }) {
// Icon(MaterialIcons.Filled.Menu, contentDescription = "More options")
Row {
Text(text = locations[pickedLocation].name)
IconButton(onClick = { expanded = !expanded }) {
Icon(imageVector = FeatherIcons.Menu, contentDescription = "Login")
// Icon(MaterialIcons.Filled.Menu, contentDescription = "More options")
}
}
DropdownMenu(
expanded = expanded,
@@ -57,7 +64,7 @@ class TravelUi {
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 playerNameUpdateResponse: PlayerNameUpdateResponseUi,
val tradeRequest: TradeRequestUi,
// TODO: tradeResponse
val travelRequest: TravelRequestUi,
val travelResponse: TravelResponseUi,
) {
@@ -16,9 +16,11 @@ import dev.geekback.dopewars.ui.LocationUi
class MarketRequestUi: DopeWarsModelUi<MarketRequest> {
@Composable
override fun view(model: MarketRequest) {
override fun view(model: MarketRequest, onRequestEdit: () -> Unit) {
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.DopeWarsModelUi
import dev.geekback.dopewars.ui.MarketUi
import dev.geekback.dopewars.ui.ViewFrame
class MarketResponseUi: DopeWarsModelUi<MarketResult> {
@Composable
override fun view(model: MarketResult) {
BorderedCard("Market Result for ${model.market.location.name}") {
MarketUi().view(model.market)
override fun view(model: MarketResult, onRequestEdit: () -> Unit) {
ViewFrame(title = "Market Result for ${model.market.location.name}",
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.ui.BorderedCard
class PlayerNameUpdateRequestUi(): DopeWarsModelUi<PlayerNameUpdateRequest> {
class PlayerNameUpdateRequestUi : DopeWarsModelUi<PlayerNameUpdateRequest> {
@Composable
override fun view(model: PlayerNameUpdateRequest) {
override fun view(model: PlayerNameUpdateRequest, onRequestEdit: () -> Unit) {
BorderedCard("Player Name Update Request for ${model.newName}") {
}
}
@@ -8,9 +8,9 @@ import dev.geekback.dopewars.ui.BorderedCard
class PlayerNameUpdateResponseUi: DopeWarsModelUi<PlayerNameUpdateResponse> {
@Composable
override fun view(model: PlayerNameUpdateResponse) {
override fun view(model: PlayerNameUpdateResponse, onRequestEdit: () -> Unit) {
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> {
@Composable
override fun view(model: TradeRequest) {
override fun view(model: TradeRequest, onRequestEdit: () -> Unit) {
BorderedCard("Trade Request for ${model.drug.name} / ${model.action} / ${model.quantity}") {
}
@@ -26,9 +26,6 @@ class TradeRequestUi: DopeWarsModelUi<TradeRequest> {
onModelUpdate: (TradeRequest) -> Unit
) {
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") {
Row {
@@ -8,7 +8,7 @@ import dev.geekback.dopewars.ui.BorderedCard
class TravelRequestUi: DopeWarsModelUi<TravelRequest> {
@Composable
override fun view(model: TravelRequest) {
override fun view(model: TravelRequest, onRequestEdit: () -> Unit) {
BorderedCard("Travel Request to ${model.destination.name}") {
}
}
@@ -17,27 +17,24 @@ import dev.geekback.dopewars.ui.MarketUi
class TravelResponseUi: DopeWarsModelUi<TravelResult> {
@Composable
override fun view(model: TravelResult) {
override fun view(model: TravelResult, onRequestEdit: () -> Unit) {
BorderedCard("Travel Result ${model.location.name}") {
LocationUi().view(model.location)
LocationUi().view(model.location) {}
Text("Day: ${model.day}")
MarketUi().view(model.market)
MarketUi().view(model.market) {}
}
}
@Composable
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") {
LocationUi().form(model.location) {
println("Update location to $it")
}
//
//
// 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
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.onClick
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import dev.geekback.dopewars.shared.DRUG_MAX_PRICE
import dev.geekback.dopewars.shared.Drug
import dev.geekback.dopewars.shared.DrugInfo
class DrugInfoUi: DopeWarsModelUi<DrugInfo> {
@Composable
override fun view(model: DrugInfo) {
BorderedCard(model.drug.name) {
Column {
Text("Price: ${model.minValue} - ${model.maxValue}")
}
}
}
@Composable
override fun form(
model: DrugInfo,
onModelUpdate: (DrugInfo) -> Unit
) {
var drug by remember { mutableStateOf(model) }
var name by remember { mutableStateOf(model.drug.name) }
var minPrice by remember { mutableFloatStateOf(model.minValue) }
var maxPrice by remember { mutableFloatStateOf(model.maxValue) }
BorderedCard("Drug Form") {
Column {
// Name
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
)
Row {
TextField(
value = minPrice.toString(),
onValueChange = { minPrice = it.toFloatOrNull() ?: minPrice },
label = { Text("Min Price") },
class DrugInfoUi : DopeWarsModelUi<DrugInfo> {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun picker(default: Drug, onSelect: (Drug) -> Unit) {
var showOptions by remember { mutableStateOf(false) }
Column {
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) }
)
TextField(
value = maxPrice.toString(),
onValueChange = { maxPrice = it.toFloatOrNull() ?: maxPrice },
label = { Text("Max Price") },
)
}
RangeSlider(
value = (minPrice)..(maxPrice),
onValueChangeFinished = { },
valueRange = 1f..DRUG_MAX_PRICE,
onValueChange = { it ->
minPrice = it.start
maxPrice = it.endInclusive
}
)
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")
}
}
}
}
}
}
@Composable
override fun view(model: DrugInfo, onRequestEdit: () -> Unit) {
ViewFrame(title = model.drug.name, onRequestEdit = onRequestEdit) {
Text("Price: ${model.minValue} - ${model.maxValue}")
}
}
@Composable
override fun form(
model: DrugInfo,
onModelUpdate: (DrugInfo) -> Unit
) {
var drug by remember { mutableStateOf(model.drug) }
var minPrice by remember { mutableIntStateOf(model.minValue) }
var maxPrice by remember { mutableIntStateOf(model.maxValue) }
FormFrame(
title = "Drug Form",
onModelUpdate = { onModelUpdate(DrugInfo(drug, minPrice, maxPrice)) },
onModelReset = {
drug = model.drug
minPrice = model.minValue
maxPrice = model.maxValue
},
onCancelEdit = { onModelUpdate(model) }) {
picker(drug) {
drug = it
}
Row {
TextField(
value = minPrice.toString(),
onValueChange = { minPrice = it.toIntOrNull() ?: minPrice },
label = { Text("Min Price") },
)
TextField(
value = maxPrice.toString(),
onValueChange = { maxPrice = it.toIntOrNull() ?: maxPrice },
label = { Text("Max Price") },
)
}
RangeSlider(
value = (minPrice.toFloat())..(maxPrice.toFloat()),
onValueChangeFinished = { },
valueRange = 1f..DRUG_MAX_PRICE,
onValueChange = {
minPrice = it.start.toInt()
maxPrice = it.endInclusive.toInt()
}
)
}
}
}
@@ -1,5 +1,6 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -7,9 +8,17 @@ import dev.geekback.dopewars.shared.InventoryItem
class InventoryUi: DopeWarsModelUi<List<InventoryItem>> {
@Composable
override fun view(model: List<InventoryItem>) {
BorderedCard("Inventory Items") {
override fun view(model: List<InventoryItem>, onRequestEdit: () -> Unit) {
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
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.geekback.dopewars.shared.InventoryItem
class InventoryItemUi: DopeWarsModelUi<InventoryItem> {
@Composable
override fun view(model: InventoryItem) {
private val drugInfoUi = DrugInfoUi()
@Composable
override fun view(model: InventoryItem, onRequestEdit: () -> Unit) {
BorderedCard("") {
Text("${model.drug.name} / ${model.quantity}g")
Button(onClick = onRequestEdit) {
Text("Edit")
}
}
}
@Composable
@@ -14,6 +30,34 @@ class InventoryItemUi: DopeWarsModelUi<InventoryItem> {
model: InventoryItem,
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
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
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.Text
import androidx.compose.material3.TextField
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.Modifier
import dev.geekback.dopewars.shared.Location
class LocationUi: DopeWarsModelUi<Location> {
class LocationUi : DopeWarsModelUi<Location> {
@OptIn(ExperimentalFoundationApi::class)
@Composable
override fun view(model: Location) {
BorderedCard("Location - ${model.name}") {
fun picker(default: Location, onSelect: (Location) -> Unit) {
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
override fun form(
model: Location,
onModelUpdate: (Location) -> Unit
) {
var name by remember { mutableStateOf(model.name) }
var location by remember { mutableStateOf(model)}
var localModel by remember { mutableStateOf(model) }
BorderedCard("Location") {
TextField(value = name, label = {
Text("Location Name")
}, onValueChange = {
name = it
})
val title = if (localModel == model) { "Location" } else { "Location *" }
Row {
Button(onClick = {
location.name = name;
onModelUpdate(location)
}) {
Text("Apply")
}
Button(onClick = {
name = location.name
}) {
Text("Cancel")
}
FormFrame(title = title,
onModelUpdate = {
onModelUpdate(localModel)
},
onModelReset = {
localModel = model
},
onCancelEdit = {
onModelUpdate(model)
}) {
picker(localModel) {
localModel = it
}
}
}
@@ -2,24 +2,34 @@ package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.Text
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.Player
class MarketUi: DopeWarsModelUi<Market> {
@Composable
override fun view(model: Market) {
override fun view(model: Market, onRequestEdit: () -> Unit) {
val prices = model.prices.toList()
BorderedCard("Market for ${model.location.name}") {
Column {
LazyColumn {
items(prices.size) { index ->
val item = prices[index];
val item = prices[index]
val name = item.first.name
val price = item.second.toFloat()
val price = item.second
Text("Drug $name $${price}")
}
}
Button(onClick = onRequestEdit) {
Text("Edit")
}
}
}
}
@@ -1,28 +1,33 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
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 kotlin.time.Instant
class PlayerUi: DopeWarsModelUi<Player> {
class PlayerUi : DopeWarsModelUi<Player> {
private val inventoryUi = InventoryUi()
private val marketUi = MarketUi()
private val locationUi = LocationUi()
@Composable
override fun view(model: Player) {
BorderedCard(model.name) {
Column {
Text("ID: ${model.id}")
Text("Cash: $${model.cash}")
Text("Debt: $${model.debt}")
Text("Health: ${model.health}")
Text("Location: ${model.location.name}")
// TODO: this is an ugly memory hack
InventoryUi().view(model.inventory)
// marketView(model.lastMarket ?: Market(location = Location("Unknown"), prices = mapOf()))
val lastTime = Instant.fromEpochSeconds(model.lastActionTime)
Text("Last Action: $lastTime")
override fun view(model: Player, onRequestEdit: () -> Unit) {
ViewFrame(
title = model.name,
onRequestEdit = onRequestEdit
) {
Text("ID: ${model.id}")
Text("Cash: $${model.cash}")
Text("Debt: $${model.debt}")
Text("Health: ${model.health}")
Text("Location: ${model.location.name}")
this.inventoryUi.view(model.inventory) {
println("Inventory Edit request")
}
// marketView(model.lastMarket ?: Market(location = Location("Unknown"), prices = mapOf()))
val lastTime = Instant.fromEpochSeconds(model.lastActionTime)
Text("Last Action: $lastTime")
}
}
@@ -31,14 +36,64 @@ class PlayerUi: DopeWarsModelUi<Player> {
model: Player,
onModelUpdate: (Player) -> Unit
) {
BorderedCard("Player ${model.name}") {
Button(onClick = { onModelUpdate(model) }) {
Text("Update")
var uuid by remember { mutableStateOf(model.id) }
var name by remember { mutableStateOf(model.name) }
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) }
FormFrame(
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!!) { }
}
}
}
}
@Composable
fun playerView(player: Player) {
}
@@ -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 {
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
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.width
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
@@ -15,6 +18,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.ui.PlayerUi
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -22,32 +26,43 @@ import kotlinx.coroutines.launch
fun App() {
val scope = rememberCoroutineScope()
var players by mutableStateOf (listOf<Player>())
var activePlayer: Player? by mutableStateOf(null)
MaterialTheme {
Scaffold(
modifier = Modifier.padding(16.dp),
content = {
Column {
// List of current Players
Button(onClick = {
println("Reload Players Now")
scope.launch {
players = AdminApi.loadPlayers()
Row {
Column(modifier = Modifier.width(250.dp)) {
// List of current Players
Button(onClick = {
scope.launch {
players = AdminApi.loadPlayers()
}
}) {
Text("Reload Players")
}
}) {
Text("Reload Players")
}
ApiControlBox {
println("Setting API BaseUrl to $it")
AdminApi.setBaseUrl(it)
}
ApiControlBox {
AdminApi.setBaseUrl(it)
}
PlayersList(players) {
println("Click on $it")
PlayersList(players) {
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 {
items(players.size) { i ->
val item = players[i];
val item = players[i]
Text("Player ${item.name}", modifier = Modifier.clickable { onPLayerSelected(item) })
}
}