working on web interface

This commit is contained in:
2026-08-24 19:12:37 -04:00
parent 92c14dfc81
commit d62575df16
42 changed files with 5683 additions and 177 deletions
+30
View File
@@ -0,0 +1,30 @@
@file:OptIn(ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
jvm()
wasmJs {
}
sourceSets {
commonMain.dependencies {
implementation(project(":shared"))
implementation(libs.compose.material3)
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.ui)
}
wasmJsMain.dependencies {
implementation(libs.wrappers.browser)
}
jvmMain.dependencies {}
}
}
@@ -0,0 +1,42 @@
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTheme(content: @Composable () -> Unit) {
Scaffold {
MaterialTheme {
content()
}
}
}
@Composable
fun BorderedCard(title: String, content: @Composable () -> Unit) {
Card(modifier = Modifier
.border(BorderStroke(width = 1.dp, color = Color.Black))
.fillMaxWidth()
.padding(16.dp)) {
Column {
Text(
modifier = Modifier.padding(16.dp),
text = title,
style = MaterialTheme.typography.titleMedium)
content()
}
}
}
@@ -0,0 +1,35 @@
package dev.geekback.dopewars.ui
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.ui.api.ApiUi
interface DopeWarsModelUi<T> {
@Composable
fun view(model: T);
@Composable
fun form(model: T, onModelUpdate: (T) -> Unit)
}
data class DopeWarsUi(
val drug: DrugInfoUi,
val inventory: InventoryUi,
val inventoryItem: InventoryItemUi,
val location: LocationUi,
val market: MarketUi,
val player: PlayerUi,
val api: ApiUi
) {
companion object {
fun new() : DopeWarsUi {
return DopeWarsUi(
drug = DrugInfoUi(),
inventory = InventoryUi(),
inventoryItem = InventoryItemUi(),
location = LocationUi(),
market = MarketUi(),
player = PlayerUi(),
api = ApiUi.new()
)
}
}
}
@@ -0,0 +1,85 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
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.IconButton
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 androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.geekback.dopewars.shared.Locations
import dev.geekback.dopewars.shared.Player
import dev.geekback.dopewars.shared.api.TravelRequest
class TravelUi {
/*
Wizard
Holds the users hand through picking a new location
*/
@Composable
fun wizard(current: Player, onTicketPunched: (TravelRequest) -> Unit) {
var stage by remember { mutableStateOf(0) }
var pickedLocation by remember { mutableStateOf(0) }
val locations = Locations.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;
when (stage) {
0 -> {
Text(text = locations[pickedLocation].name)
IconButton(onClick = { expanded = !expanded }) {
// Icon(MaterialIcons.Filled.Menu, contentDescription = "More options")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
locations.forEach {
DropdownMenuItem(
text = { Text("$it.name / $index")},
onClick = {
expanded = false
pickedLocation = (index - 1)
println("location index to $index")
}
)
index += 1;
}
}
Button(onClick = {
stage += 1
}) {
Text("Next Step")
}
}
1 -> {
Text("Stage 1")
Button(onClick = {
println("Selected Location - $pickedLocation")
onTicketPunched(TravelRequest(locations[pickedLocation]))
}) {
Text("Finish")
}
}
}
}
}
*/
// }
}
}
@@ -0,0 +1,26 @@
package dev.geekback.dopewars.ui.api
data class ApiUi(
val marketRequest: MarketRequestUi,
val marketResponse: MarketResponseUi,
val playerNameUpdateRequest: PlayerNameUpdateRequestUi,
val playerNameUpdateResponse: PlayerNameUpdateResponseUi,
val tradeRequest: TradeRequestUi,
// TODO: tradeResponse
val travelRequest: TravelRequestUi,
val travelResponse: TravelResponseUi,
) {
companion object {
fun new(): ApiUi {
return ApiUi(
marketRequest = MarketRequestUi(),
marketResponse = MarketResponseUi(),
playerNameUpdateRequest = PlayerNameUpdateRequestUi(),
playerNameUpdateResponse = PlayerNameUpdateResponseUi(),
tradeRequest = TradeRequestUi(),
travelRequest = TravelRequestUi(),
travelResponse = TravelResponseUi()
)
}
}
}
@@ -0,0 +1,47 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.api.MarketRequest
import dev.geekback.dopewars.ui.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.LocationUi
class MarketRequestUi: DopeWarsModelUi<MarketRequest> {
@Composable
override fun view(model: MarketRequest) {
BorderedCard("Market Request for ${model.location.name}") {
LocationUi().view(model.location)
}
}
@Composable
override fun form(
model: MarketRequest,
onModelUpdate: (MarketRequest) -> Unit
) {
var localModel by remember { mutableStateOf(model) }
BorderedCard("Market Request Form") {
Column {
LocationUi().form(model.location) {
localModel = MarketRequest(location = it)
}
Row {
Button(onClick = { onModelUpdate(localModel) }) {
Text("Apply")
}
Button(onClick = { localModel = model }) {
Text("Cancel")
}
}
}
}
}
}
@@ -0,0 +1,29 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.runtime.Composable
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
class MarketResponseUi: DopeWarsModelUi<MarketResult> {
@Composable
override fun view(model: MarketResult) {
BorderedCard("Market Result for ${model.market.location.name}") {
MarketUi().view(model.market)
}
}
@Composable
override fun form(
model: MarketResult,
onModelUpdate: (MarketResult) -> Unit
) {
BorderedCard("Market Result Form") {
MarketUi().form(model.market) {
onModelUpdate(model)
}
}
}
}
@@ -0,0 +1,25 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.shared.api.PlayerNameUpdateRequest
import dev.geekback.dopewars.ui.BorderedCard
class PlayerNameUpdateRequestUi(): DopeWarsModelUi<PlayerNameUpdateRequest> {
@Composable
override fun view(model: PlayerNameUpdateRequest) {
BorderedCard("Player Name Update Request for ${model.newName}") {
}
}
@Composable
override fun form(model: PlayerNameUpdateRequest, onModelUpdate: (PlayerNameUpdateRequest) -> Unit) {
Text("Player Name Update Request Form")
Button(onClick = { onModelUpdate(model) }) {
Text("Update with same data")
}
}
}
@@ -0,0 +1,26 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.PlayerUi
import dev.geekback.dopewars.shared.api.PlayerNameUpdateResponse
import dev.geekback.dopewars.ui.BorderedCard
class PlayerNameUpdateResponseUi: DopeWarsModelUi<PlayerNameUpdateResponse> {
@Composable
override fun view(model: PlayerNameUpdateResponse) {
BorderedCard("Player Name Update Response") {
PlayerUi().view(model.updatedPlayer)
}
}
@Composable
override fun form(
model: PlayerNameUpdateResponse,
onModelUpdate: (PlayerNameUpdateResponse) -> Unit
) {
PlayerUi().form(model.updatedPlayer) {
onModelUpdate(model)
}
}
}
@@ -0,0 +1,50 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Row
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.api.TradeRequest
import dev.geekback.dopewars.ui.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
class TradeRequestUi: DopeWarsModelUi<TradeRequest> {
@Composable
override fun view(model: TradeRequest) {
BorderedCard("Trade Request for ${model.drug.name} / ${model.action} / ${model.quantity}") {
}
}
@Composable
override fun form(
model: 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 {
Button(onClick = {
onModelUpdate(localModel)
println("Trade Request Form Apply Clicked")
}) {
Text("Apply")
}
Button(onClick = {
localModel = model
}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,25 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.shared.api.TravelRequest
import dev.geekback.dopewars.ui.BorderedCard
class TravelRequestUi: DopeWarsModelUi<TravelRequest> {
@Composable
override fun view(model: TravelRequest) {
BorderedCard("Travel Request to ${model.destination.name}") {
}
}
@Composable
override fun form(
model: TravelRequest,
onModelUpdate: (TravelRequest) -> Unit
) {
BorderedCard("Travel Request Form") {
Text("Location Picker goes here.")
}
}
}
@@ -0,0 +1,56 @@
package dev.geekback.dopewars.ui.api
import androidx.compose.foundation.layout.Row
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.api.TravelResult
import dev.geekback.dopewars.ui.BorderedCard
import dev.geekback.dopewars.ui.DopeWarsModelUi
import dev.geekback.dopewars.ui.LocationUi
import dev.geekback.dopewars.ui.MarketUi
class TravelResponseUi: DopeWarsModelUi<TravelResult> {
@Composable
override fun view(model: TravelResult) {
BorderedCard("Travel Result ${model.location.name}") {
LocationUi().view(model.location)
Text("Day: ${model.day}")
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 {
// Button(onClick = {}) {
// Text("Apply")
// }
//
// Button(onClick = {}) {
// Text("Cancel")
// }
// }
}
}
}
@@ -0,0 +1,92 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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") },
)
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")
}
}
}
}
}
}
@@ -0,0 +1,28 @@
package dev.geekback.dopewars.ui
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.shared.InventoryItem
class InventoryUi: DopeWarsModelUi<List<InventoryItem>> {
@Composable
override fun view(model: List<InventoryItem>) {
BorderedCard("Inventory Items") {
}
}
@Composable
override fun form(
model: List<InventoryItem>,
onModelUpdate: (List<InventoryItem>) -> Unit
) {
BorderedCard("Inventory Items Form") {
Button(onClick = { onModelUpdate(model) }) {
Text("Update")
}
}
}
}
@@ -0,0 +1,19 @@
package dev.geekback.dopewars.ui
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.shared.InventoryItem
class InventoryItemUi: DopeWarsModelUi<InventoryItem> {
@Composable
override fun view(model: InventoryItem) {
}
@Composable
override fun form(
model: InventoryItem,
onModelUpdate: (InventoryItem) -> Unit
) {
TODO("Not yet implemented")
}
}
@@ -0,0 +1,54 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
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 dev.geekback.dopewars.shared.Location
class LocationUi: DopeWarsModelUi<Location> {
@Composable
override fun view(model: Location) {
BorderedCard("Location - ${model.name}") {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun form(
model: Location,
onModelUpdate: (Location) -> Unit
) {
var name by remember { mutableStateOf(model.name) }
var location by remember { mutableStateOf(model)}
BorderedCard("Location") {
TextField(value = name, label = {
Text("Location Name")
}, onValueChange = {
name = it
})
Row {
Button(onClick = {
location.name = name;
onModelUpdate(location)
}) {
Text("Apply")
}
Button(onClick = {
name = location.name
}) {
Text("Cancel")
}
}
}
}
}
@@ -0,0 +1,37 @@
package dev.geekback.dopewars.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.geekback.dopewars.shared.Market
class MarketUi: DopeWarsModelUi<Market> {
@Composable
override fun view(model: Market) {
val prices = model.prices.toList()
BorderedCard("Market for ${model.location.name}") {
Column {
LazyColumn {
items(prices.size) { index ->
val item = prices[index];
val name = item.first.name
val price = item.second.toFloat()
Text("Drug $name $${price}")
}
}
}
}
}
@Composable
override fun form(
model: Market,
onModelUpdate: (Market) -> Unit
) {
BorderedCard("Market for ${model.location.name}") {
Text("Market goes here.")
}
}
}
@@ -0,0 +1,44 @@
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 dev.geekback.dopewars.shared.Player
import kotlin.time.Instant
class PlayerUi: DopeWarsModelUi<Player> {
@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")
}
}
}
@Composable
override fun form(
model: Player,
onModelUpdate: (Player) -> Unit
) {
BorderedCard("Player ${model.name}") {
Button(onClick = { onModelUpdate(model) }) {
Text("Update")
}
}
}
}
@Composable
fun playerView(player: Player) {
}