all done
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import javafx.application.Application
|
||||
import javafx.stage.Stage
|
||||
import model.Session
|
||||
import view.AuthView
|
||||
import view.MainView
|
||||
|
||||
class App : Application() {
|
||||
|
||||
override fun start(stage: Stage) {
|
||||
AuthView(stage) {
|
||||
val mainStage = Stage()
|
||||
MainView(mainStage).show()
|
||||
}.show()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
Session.client?.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
Application.launch(App::class.java)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package controller
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import localization.AppLocale
|
||||
import model.Session
|
||||
import network.AuthType
|
||||
import guiclient.network.ClientProvider
|
||||
import javafx.application.Platform
|
||||
|
||||
class AuthController {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
fun authenticate(
|
||||
host: String,
|
||||
port: Int,
|
||||
username: String,
|
||||
password: String,
|
||||
isLogin: Boolean,
|
||||
onResult: (Boolean, String) -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
Session.client?.close()
|
||||
|
||||
val provider = ClientProvider(host, port)
|
||||
|
||||
val type = if (isLogin) AuthType.LOGIN else AuthType.REGISTER
|
||||
val response = provider.auth(username, password, type)
|
||||
|
||||
if (response == null) {
|
||||
Platform.runLater { onResult(false, AppLocale.get("auth.error.server")) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
provider.setToken(response.accessToken ?: "")
|
||||
provider.setUUID(response.id ?: "")
|
||||
Session.client = provider
|
||||
Session.username = username
|
||||
Session.userId = response.id?.let { kotlin.runCatching { java.util.UUID.fromString(it) }.getOrNull() }
|
||||
Platform.runLater { onResult(true, "") }
|
||||
} else {
|
||||
val errorKey = if (isLogin) "auth.error.failed" else "auth.error.register.failed"
|
||||
Platform.runLater { onResult(false, response.message.ifEmpty { AppLocale.get(errorKey) }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package controller
|
||||
|
||||
import javafx.application.Platform
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.coroutines.*
|
||||
import localization.AppLocale
|
||||
import model.HumanBeingProperty
|
||||
import model.Session
|
||||
import util.StreamUtils
|
||||
import java.util.Comparator
|
||||
import java.util.UUID
|
||||
|
||||
class MainController {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
private var pollingJob: Job? = null
|
||||
|
||||
val filteredList: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
|
||||
|
||||
private var currentFilters: Map<String, String> = emptyMap()
|
||||
private var currentComparators: List<Comparator<HumanBeingProperty>> = emptyList()
|
||||
|
||||
private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
|
||||
reapplyFiltersAndSort()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
Session.collection.addListener(collectionListener)
|
||||
startPolling()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
pollingJob?.cancel()
|
||||
scope.cancel()
|
||||
Session.collection.removeListener(collectionListener)
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
pollingJob = scope.launch {
|
||||
while (isActive) {
|
||||
delay(3000)
|
||||
refreshCollection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestRefresh() {
|
||||
scope.launch {
|
||||
refreshCollection()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshCollection() {
|
||||
val response = Session.client?.execute("show") ?: return
|
||||
|
||||
val currentIds = Session.collection.map { it.id.value }.toSet()
|
||||
|
||||
val serverItems = parseShowResponse(response.message) ?: return
|
||||
val serverIds = serverItems.map { it.id.value }.toSet()
|
||||
|
||||
val toRemove = Session.collection.filter { it.id.value !in serverIds }
|
||||
val toAdd = serverItems.filter { it.id.value !in currentIds }
|
||||
val toUpdate = serverItems.filter { item ->
|
||||
val existing = Session.collection.find { it.id.value == item.id.value }
|
||||
existing != null && existing.name.value != item.name.value
|
||||
}
|
||||
|
||||
Platform.runLater {
|
||||
Session.collection.removeAll(toRemove)
|
||||
Session.collection.addAll(toAdd)
|
||||
for (updated in toUpdate) {
|
||||
val idx = Session.collection.indexOfFirst { it.id.value == updated.id.value }
|
||||
if (idx >= 0) {
|
||||
Session.collection[idx] = updated
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseShowResponse(message: String): List<HumanBeingProperty>? {
|
||||
if (message.isBlank()) return emptyList()
|
||||
val lines = message.lines().filter { it.isNotBlank() }
|
||||
|
||||
val items = mutableListOf<HumanBeingProperty>()
|
||||
var currentLines = mutableListOf<String>()
|
||||
var braceCount = 0
|
||||
|
||||
for (line in lines) {
|
||||
currentLines.add(line)
|
||||
braceCount += line.count { it == '{' }
|
||||
braceCount -= line.count { it == '}' }
|
||||
if (braceCount == 0 && currentLines.isNotEmpty()) {
|
||||
val parsed = parseHumanBeingBlock(currentLines)
|
||||
if (parsed != null) items.add(parsed)
|
||||
currentLines.clear()
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
private fun parseHumanBeingBlock(lines: List<String>): HumanBeingProperty? {
|
||||
val text = lines.joinToString("\n")
|
||||
|
||||
val id = extractValue(text, "id\\s*=\\s*([\\w-]+)")?.let { safeUUID(it) }
|
||||
val ownerId = extractValue(text, "owner\\s*=\\s*([\\w-]+)")?.let { safeUUID(it) } ?: return null
|
||||
val name = extractValue(text, "name\\s*=\\s*(.+?)\\s*\\(a\\.k\\.a\\. Ryan Gosling\\)") ?: return null
|
||||
val coordX = extractValue(text, "x=(-?\\d+)")?.toLongOrNull() ?: 0L
|
||||
val coordY = extractValue(text, "y=(-?\\d+)")?.toIntOrNull() ?: 0
|
||||
val creationDate = extractValue(text, "creationDate\\s*=\\s*(\\S+)")?.let { safeLocalDate(it) }
|
||||
val realHero = extractValue(text, "realHero\\s*=\\s*(\\w+)")?.toBooleanStrictOrNull() ?: false
|
||||
val hasToothpick = extractValue(text, "hasToothpick\\s*=\\s*(\\w+)")?.toBooleanStrictOrNull() ?: false
|
||||
val impactSpeed = extractValue(text, "impactSpeed\\s*=\\s*(-?\\d+\\.?\\d*)")?.toDoubleOrNull() ?: 0.0
|
||||
val soundtrackName = extractValue(text, "soundtrackName\\s*=\\s*(.+)") ?: ""
|
||||
val minutesOfWaiting = extractValue(text, "minutesOfWait\\s*=\\s*(-?\\d+\\.?\\d*)")?.toFloatOrNull() ?: 0f
|
||||
val weaponType = extractValue(text, "weaponType\\s*=\\s*(\\w+)")?.let { safeWeaponType(it) }
|
||||
val carCool = extractValue(text, "car\\.cool\\s*=\\s*(\\w+)")?.toBooleanStrictOrNull() ?: false
|
||||
|
||||
val date = creationDate ?: java.time.LocalDate.now()
|
||||
|
||||
val human = models.HumanBeing(
|
||||
id = id,
|
||||
ownerId = ownerId,
|
||||
name = name,
|
||||
coordinates = models.Coordinates(coordX, coordY),
|
||||
creationDate = date,
|
||||
realHero = realHero,
|
||||
hasToothpick = hasToothpick,
|
||||
impactSpeed = impactSpeed,
|
||||
soundtrackName = soundtrackName,
|
||||
minutesOfWaiting = minutesOfWaiting,
|
||||
weaponType = weaponType,
|
||||
car = models.Car(carCool),
|
||||
)
|
||||
return HumanBeingProperty(human)
|
||||
}
|
||||
|
||||
private fun extractValue(text: String, pattern: String): String? {
|
||||
val regex = Regex(pattern)
|
||||
return regex.find(text)?.groupValues?.getOrNull(1)
|
||||
}
|
||||
|
||||
private fun safeUUID(value: String): UUID? {
|
||||
return try { UUID.fromString(value) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
private fun safeLocalDate(value: String): java.time.LocalDate? {
|
||||
return try { java.time.LocalDate.parse(value) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
private fun safeWeaponType(value: String): models.WeaponType? {
|
||||
return try { models.WeaponType.valueOf(value.uppercase()) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun updateFilters(filters: Map<String, String>) {
|
||||
currentFilters = filters
|
||||
reapplyFiltersAndSort()
|
||||
}
|
||||
|
||||
fun updateComparators(comparators: List<Comparator<HumanBeingProperty>>) {
|
||||
currentComparators = comparators
|
||||
reapplyFiltersAndSort()
|
||||
}
|
||||
|
||||
private fun reapplyFiltersAndSort() {
|
||||
val result = StreamUtils.applyFiltersAndSort(
|
||||
Session.collection,
|
||||
currentFilters,
|
||||
currentComparators,
|
||||
)
|
||||
Platform.runLater {
|
||||
filteredList.setAll(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun executeCommand(
|
||||
name: String,
|
||||
args: List<String> = emptyList(),
|
||||
humanBeing: models.HumanBeing? = null,
|
||||
callback: (Boolean, String) -> Unit = { _, _ -> },
|
||||
) {
|
||||
scope.launch {
|
||||
val response = Session.client?.execute(name, args, humanBeing)
|
||||
val success = response?.success ?: false
|
||||
val message = response?.message ?: AppLocale.get("auth.error.server")
|
||||
Platform.runLater { callback(success, message) }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteObject(humanBeingProperty: HumanBeingProperty, callback: (Boolean, String) -> Unit) {
|
||||
val id = humanBeingProperty.id.value ?: return
|
||||
executeCommand(name = "remove_by_id", args = listOf(id.toString()), callback = callback)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package guiclient.network
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import models.HumanBeing
|
||||
import network.NetworkManager as ClientNetworkManager
|
||||
import network.AuthRequest
|
||||
import network.AuthResponse
|
||||
import network.AuthType
|
||||
import network.Request
|
||||
import network.Response
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
|
||||
class ClientProvider(
|
||||
host: String,
|
||||
port: Int,
|
||||
) {
|
||||
private val net = ClientNetworkManager(host, port)
|
||||
|
||||
suspend fun auth(login: String, password: String, type: AuthType): AuthResponse? =
|
||||
withContext(Dispatchers.IO) {
|
||||
net.sendRequest(AuthRequest(login, md5Hash(password), type))
|
||||
}
|
||||
|
||||
suspend fun execute(commandName: String, args: List<String> = emptyList(), humanBeing: HumanBeing? = null): Response? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request = Request(
|
||||
commandName = commandName,
|
||||
args = args,
|
||||
humanBeing = humanBeing,
|
||||
jwt = net.getToken()
|
||||
)
|
||||
net.sendRequest(request)
|
||||
}
|
||||
|
||||
fun getToken(): String = net.getToken()
|
||||
|
||||
fun setToken(token: String) = net.setToken(token)
|
||||
|
||||
fun clearToken() = net.clearToken()
|
||||
|
||||
fun getUUID(): UUID = net.getUUID()
|
||||
|
||||
fun setUUID(id: String) = net.setUUID(id)
|
||||
|
||||
fun close() = net.close()
|
||||
|
||||
private fun md5Hash(input: String): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val hashBytes = md.digest(input.toByteArray())
|
||||
return hashBytes.joinToString("") { String.format("%02x", it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package localization
|
||||
|
||||
import javafx.beans.property.ObjectProperty
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.scene.control.Labeled
|
||||
import java.util.Locale
|
||||
|
||||
object AppLocale {
|
||||
|
||||
val currentLocale: ObjectProperty<Locale> = SimpleObjectProperty(Locale("ru"))
|
||||
|
||||
private val strings: Map<Locale, Map<String, String>> = mapOf(
|
||||
Locale("ru") to mapOf(
|
||||
"auth.title" to "Вход / Регистрация",
|
||||
"auth.login" to "Вход",
|
||||
"auth.register" to "Регистрация",
|
||||
"auth.username" to "Имя пользователя",
|
||||
"auth.password" to "Пароль",
|
||||
"auth.host" to "Сервер",
|
||||
"auth.port" to "Порт",
|
||||
"auth.error.invalid.port" to "Некорректный порт (1–65535)",
|
||||
"auth.submit.login" to "Войти",
|
||||
"auth.submit.register" to "Зарегистрироваться",
|
||||
"auth.error.empty" to "Заполните все поля",
|
||||
"auth.error.failed" to "Ошибка входа. Проверьте логин и пароль.",
|
||||
"auth.error.register.failed" to "Ошибка регистрации. Возможно, пользователь уже существует.",
|
||||
"auth.error.server" to "Сервер недоступен",
|
||||
"auth.success.login" to "Успешный вход",
|
||||
"auth.success.register" to "Успешная регистрация",
|
||||
"main.title" to "Коллекция HumanBeing",
|
||||
"main.user" to "Пользователь",
|
||||
"main.logout" to "Выйти",
|
||||
"main.locale" to "Язык",
|
||||
"main.filter" to "Фильтр",
|
||||
"visualization.title" to "Визуализация",
|
||||
"table.name" to "Имя",
|
||||
"table.coordinates.x" to "X",
|
||||
"table.coordinates.y" to "Y",
|
||||
"table.creationDate" to "Дата создания",
|
||||
"table.realHero" to "Герой",
|
||||
"table.hasToothpick" to "Зубочистка",
|
||||
"table.impactSpeed" to "Скорость",
|
||||
"table.soundtrackName" to "Саундтрек",
|
||||
"table.minutesOfWaiting" to "Минуты ожидания",
|
||||
"table.weaponType" to "Оружие",
|
||||
"table.car.cool" to "Крутая тачка",
|
||||
"table.owner" to "Владелец",
|
||||
"button.add" to "Добавить",
|
||||
"button.edit" to "Редактировать",
|
||||
"button.delete" to "Удалить",
|
||||
"button.refresh" to "Обновить",
|
||||
"dialog.title.add" to "Добавление объекта",
|
||||
"dialog.title.edit" to "Редактирование объекта",
|
||||
"dialog.ok" to "OK",
|
||||
"dialog.cancel" to "Отмена",
|
||||
"dialog.error.validation" to "Ошибка валидации",
|
||||
"dialog.error.name" to "Имя не может быть пустым",
|
||||
"dialog.error.number" to "Некорректное число",
|
||||
"dialog.error.no.user" to "Ошибка: пользователь не идентифицирован",
|
||||
"info.title" to "Информация об объекте",
|
||||
"info.owner" to "Владелец",
|
||||
"info.id" to "ID",
|
||||
"delete.confirm" to "Удалить этот объект?",
|
||||
"delete.confirm.title" to "Подтверждение удаления",
|
||||
"error.no.selection" to "Объект не выбран",
|
||||
"error.not.owner" to "Вы не владелец этого объекта",
|
||||
),
|
||||
Locale("nl") to mapOf(
|
||||
"auth.title" to "Inloggen / Registreren",
|
||||
"auth.login" to "Inloggen",
|
||||
"auth.register" to "Registreren",
|
||||
"auth.username" to "Gebruikersnaam",
|
||||
"auth.password" to "Wachtwoord",
|
||||
"auth.host" to "Server",
|
||||
"auth.port" to "Poort",
|
||||
"auth.error.invalid.port" to "Ongeldige poort (1–65535)",
|
||||
"auth.submit.login" to "Inloggen",
|
||||
"auth.submit.register" to "Registreren",
|
||||
"auth.error.empty" to "Vul alle velden in",
|
||||
"auth.error.failed" to "Inloggen mislukt. Controleer uw gegevens.",
|
||||
"auth.error.register.failed" to "Registratie mislukt. Gebruiker bestaat mogelijk al.",
|
||||
"auth.error.server" to "Server niet bereikbaar",
|
||||
"auth.success.login" to "Succesvol ingelogd",
|
||||
"auth.success.register" to "Succesvol geregistreerd",
|
||||
"main.title" to "HumanBeing Collectie",
|
||||
"main.user" to "Gebruiker",
|
||||
"main.logout" to "Uitloggen",
|
||||
"main.locale" to "Taal",
|
||||
"main.filter" to "Filter",
|
||||
"visualization.title" to "Visualisatie",
|
||||
"table.name" to "Naam",
|
||||
"table.coordinates.x" to "X",
|
||||
"table.coordinates.y" to "Y",
|
||||
"table.creationDate" to "Aanmaakdatum",
|
||||
"table.realHero" to "Held",
|
||||
"table.hasToothpick" to "Tandenstoker",
|
||||
"table.impactSpeed" to "Snelheid",
|
||||
"table.soundtrackName" to "Soundtrack",
|
||||
"table.minutesOfWaiting" to "Wachtminuten",
|
||||
"table.weaponType" to "Wapen",
|
||||
"table.car.cool" to "Coole auto",
|
||||
"table.owner" to "Eigenaar",
|
||||
"button.add" to "Toevoegen",
|
||||
"button.edit" to "Bewerken",
|
||||
"button.delete" to "Verwijderen",
|
||||
"button.refresh" to "Vernieuwen",
|
||||
"dialog.title.add" to "Object toevoegen",
|
||||
"dialog.title.edit" to "Object bewerken",
|
||||
"dialog.ok" to "OK",
|
||||
"dialog.cancel" to "Annuleren",
|
||||
"dialog.error.validation" to "Validatiefout",
|
||||
"dialog.error.name" to "Naam mag niet leeg zijn",
|
||||
"dialog.error.number" to "Ongeldig nummer",
|
||||
"dialog.error.no.user" to "Fout: gebruiker niet geïdentificeerd",
|
||||
"info.title" to "Objectinformatie",
|
||||
"info.owner" to "Eigenaar",
|
||||
"info.id" to "ID",
|
||||
"delete.confirm" to "Dit object verwijderen?",
|
||||
"delete.confirm.title" to "Verwijderen bevestigen",
|
||||
"error.no.selection" to "Geen object geselecteerd",
|
||||
"error.not.owner" to "U bent niet de eigenaar van dit object",
|
||||
),
|
||||
Locale("hu") to mapOf(
|
||||
"auth.title" to "Bejelentkezés / Regisztráció",
|
||||
"auth.login" to "Bejelentkezés",
|
||||
"auth.register" to "Regisztráció",
|
||||
"auth.username" to "Felhasználónév",
|
||||
"auth.password" to "Jelszó",
|
||||
"auth.host" to "Szerver",
|
||||
"auth.port" to "Port",
|
||||
"auth.error.invalid.port" to "Érvénytelen port (1–65535)",
|
||||
"auth.submit.login" to "Belépés",
|
||||
"auth.submit.register" to "Regisztrálás",
|
||||
"auth.error.empty" to "Töltse ki az összes mezőt",
|
||||
"auth.error.failed" to "Sikertelen bejelentkezés. Ellenőrizze adatait.",
|
||||
"auth.error.register.failed" to "Sikertelen regisztráció. Lehet, hogy a felhasználó már létezik.",
|
||||
"auth.error.server" to "Szerver nem elérhető",
|
||||
"auth.success.login" to "Sikeres bejelentkezés",
|
||||
"auth.success.register" to "Sikeres regisztráció",
|
||||
"main.title" to "HumanBeing Gyűjtemény",
|
||||
"main.user" to "Felhasználó",
|
||||
"main.logout" to "Kijelentkezés",
|
||||
"main.locale" to "Nyelv",
|
||||
"main.filter" to "Szűrő",
|
||||
"visualization.title" to "Vizualizáció",
|
||||
"table.name" to "Név",
|
||||
"table.coordinates.x" to "X",
|
||||
"table.coordinates.y" to "Y",
|
||||
"table.creationDate" to "Létrehozás dátuma",
|
||||
"table.realHero" to "Hős",
|
||||
"table.hasToothpick" to "Fogpiszkáló",
|
||||
"table.impactSpeed" to "Sebesség",
|
||||
"table.soundtrackName" to "Soundtrack",
|
||||
"table.minutesOfWaiting" to "Várakozási percek",
|
||||
"table.weaponType" to "Fegyver",
|
||||
"table.car.cool" to "Menő autó",
|
||||
"table.owner" to "Tulajdonos",
|
||||
"button.add" to "Hozzáadás",
|
||||
"button.edit" to "Szerkesztés",
|
||||
"button.delete" to "Törlés",
|
||||
"button.refresh" to "Frissítés",
|
||||
"dialog.title.add" to "Objektum hozzáadása",
|
||||
"dialog.title.edit" to "Objektum szerkesztése",
|
||||
"dialog.ok" to "OK",
|
||||
"dialog.cancel" to "Mégse",
|
||||
"dialog.error.validation" to "Érvényesítési hiba",
|
||||
"dialog.error.name" to "A név nem lehet üres",
|
||||
"dialog.error.number" to "Érvénytelen szám",
|
||||
"dialog.error.no.user" to "Hiba: a felhasználó nincs azonosítva",
|
||||
"info.title" to "Objektum információ",
|
||||
"info.owner" to "Tulajdonos",
|
||||
"info.id" to "ID",
|
||||
"delete.confirm" to "Törli ezt az objektumot?",
|
||||
"delete.confirm.title" to "Törlés megerősítése",
|
||||
"error.no.selection" to "Nincs objektum kiválasztva",
|
||||
"error.not.owner" to "Ön nem a tulajdonosa ennek az objektumnak",
|
||||
),
|
||||
Locale("en", "AU") to mapOf(
|
||||
"auth.title" to "Sign In / Sign Up",
|
||||
"auth.login" to "Sign In",
|
||||
"auth.register" to "Sign Up",
|
||||
"auth.username" to "Username",
|
||||
"auth.password" to "Password",
|
||||
"auth.host" to "Server",
|
||||
"auth.port" to "Port",
|
||||
"auth.error.invalid.port" to "Invalid port (1–65535)",
|
||||
"auth.submit.login" to "Sign In",
|
||||
"auth.submit.register" to "Sign Up",
|
||||
"auth.error.empty" to "Fill in all fields, mate",
|
||||
"auth.error.failed" to "Login failed. Check ya credentials.",
|
||||
"auth.error.register.failed" to "Registration failed. Reckon that user already exists.",
|
||||
"auth.error.server" to "Server's cactus",
|
||||
"auth.success.login" to "G'day! Logged in successfully",
|
||||
"auth.success.register" to "Registered, bonza!",
|
||||
"main.title" to "HumanBeing Collection",
|
||||
"main.user" to "User",
|
||||
"main.logout" to "Logout",
|
||||
"main.locale" to "Language",
|
||||
"main.filter" to "Filter",
|
||||
"visualization.title" to "Visualisation",
|
||||
"table.name" to "Name",
|
||||
"table.coordinates.x" to "X",
|
||||
"table.coordinates.y" to "Y",
|
||||
"table.creationDate" to "Creation Date",
|
||||
"table.realHero" to "Hero",
|
||||
"table.hasToothpick" to "Toothpick",
|
||||
"table.impactSpeed" to "Impact Speed",
|
||||
"table.soundtrackName" to "Soundtrack",
|
||||
"table.minutesOfWaiting" to "Minutes of Waitin'",
|
||||
"table.weaponType" to "Weapon",
|
||||
"table.car.cool" to "Carn't",
|
||||
"table.owner" to "Owner",
|
||||
"button.add" to "Add",
|
||||
"button.edit" to "Edit",
|
||||
"button.delete" to "Delete",
|
||||
"button.refresh" to "Refresh",
|
||||
"dialog.title.add" to "Add Object",
|
||||
"dialog.title.edit" to "Edit Object",
|
||||
"dialog.ok" to "OK",
|
||||
"dialog.cancel" to "Cancel",
|
||||
"dialog.error.validation" to "Validation Error",
|
||||
"dialog.error.name" to "Name can't be empty",
|
||||
"dialog.error.number" to "Invalid number, mate",
|
||||
"dialog.error.no.user" to "Error: user not identified, mate",
|
||||
"info.title" to "Object Info",
|
||||
"info.owner" to "Owner",
|
||||
"info.id" to "ID",
|
||||
"delete.confirm" to "Reckon you wanna delete this?",
|
||||
"delete.confirm.title" to "Confirm Deletion",
|
||||
"error.no.selection" to "No object selected",
|
||||
"error.not.owner" to "You're not the owner of this object",
|
||||
),
|
||||
)
|
||||
|
||||
fun get(key: String): String {
|
||||
return strings[currentLocale.value]?.get(key) ?: key
|
||||
}
|
||||
|
||||
fun bind(property: javafx.beans.property.Property<String>, key: String) {
|
||||
property.value = get(key)
|
||||
currentLocale.addListener { _, _, _ ->
|
||||
property.value = get(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun bindLabel(labeled: Labeled, key: String) {
|
||||
bind(labeled.textProperty(), key)
|
||||
}
|
||||
|
||||
fun bindPrompt(textField: javafx.scene.control.TextInputControl, key: String) {
|
||||
bind(textField.promptTextProperty(), key)
|
||||
}
|
||||
|
||||
fun availableLocales(): List<Locale> = strings.keys.toList()
|
||||
|
||||
fun localeDisplayName(locale: Locale): String = when (locale) {
|
||||
Locale("ru") -> "Русский"
|
||||
Locale("nl") -> "Nederlands"
|
||||
Locale("hu") -> "Magyar"
|
||||
Locale("en", "AU") -> "English (Australia)"
|
||||
else -> locale.displayName
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import javafx.beans.property.*
|
||||
import models.Car
|
||||
import models.Coordinates
|
||||
import models.HumanBeing
|
||||
import models.WeaponType
|
||||
import java.time.LocalDate
|
||||
import java.util.UUID
|
||||
|
||||
class HumanBeingProperty(humanBeing: HumanBeing) {
|
||||
val id = SimpleObjectProperty<UUID?>(humanBeing.id)
|
||||
val ownerId = SimpleObjectProperty<UUID>(humanBeing.ownerId)
|
||||
val name = SimpleStringProperty(humanBeing.name)
|
||||
val coordinateX = SimpleObjectProperty<Long>(humanBeing.coordinates.x)
|
||||
val coordinateY = SimpleObjectProperty<Int>(humanBeing.coordinates.y)
|
||||
val creationDate = SimpleObjectProperty<LocalDate>(humanBeing.creationDate)
|
||||
val realHero = SimpleBooleanProperty(humanBeing.realHero)
|
||||
val hasToothpick = SimpleBooleanProperty(humanBeing.hasToothpick)
|
||||
val impactSpeed = SimpleObjectProperty<Double>(humanBeing.impactSpeed)
|
||||
val soundtrackName = SimpleStringProperty(humanBeing.soundtrackName)
|
||||
val minutesOfWaiting = SimpleObjectProperty<Float>(humanBeing.minutesOfWaiting)
|
||||
val weaponType = SimpleObjectProperty<WeaponType?>(humanBeing.weaponType)
|
||||
val carCool = SimpleBooleanProperty(humanBeing.car.cool)
|
||||
|
||||
fun toHumanBeing(): HumanBeing = HumanBeing(
|
||||
id = id.value,
|
||||
ownerId = ownerId.value,
|
||||
name = name.value,
|
||||
coordinates = Coordinates(coordinateX.value, coordinateY.value),
|
||||
creationDate = creationDate.value,
|
||||
realHero = realHero.value,
|
||||
hasToothpick = hasToothpick.value,
|
||||
impactSpeed = impactSpeed.value,
|
||||
soundtrackName = soundtrackName.value,
|
||||
minutesOfWaiting = minutesOfWaiting.value,
|
||||
weaponType = weaponType.value,
|
||||
car = Car(carCool.value),
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromHumanBeing(h: HumanBeing): HumanBeingProperty = HumanBeingProperty(h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import guiclient.network.ClientProvider
|
||||
import java.util.UUID
|
||||
|
||||
object Session {
|
||||
var client: ClientProvider? = null
|
||||
|
||||
var token: String = ""
|
||||
var userId: UUID? = null
|
||||
var username: String = ""
|
||||
|
||||
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
|
||||
|
||||
val userIdFilter: UUID?
|
||||
get() = userId
|
||||
|
||||
fun isOwnObject(ownerId: UUID): Boolean = userId == ownerId
|
||||
|
||||
fun clear() {
|
||||
token = ""
|
||||
userId = null
|
||||
username = ""
|
||||
collection.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package util
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import model.HumanBeingProperty
|
||||
import java.util.Comparator
|
||||
import java.util.stream.Collectors
|
||||
import java.util.stream.Stream
|
||||
|
||||
object StreamUtils {
|
||||
|
||||
fun filter(
|
||||
items: List<HumanBeingProperty>,
|
||||
propertyExtractor: (HumanBeingProperty) -> String,
|
||||
query: String,
|
||||
): Stream<HumanBeingProperty> {
|
||||
if (query.isBlank()) return items.stream()
|
||||
val lower = query.trim().lowercase()
|
||||
return items.stream()
|
||||
.filter { propertyExtractor(it).lowercase().contains(lower) }
|
||||
}
|
||||
|
||||
fun sort(
|
||||
stream: Stream<HumanBeingProperty>,
|
||||
comparator: Comparator<HumanBeingProperty>?,
|
||||
): Stream<HumanBeingProperty> {
|
||||
if (comparator == null) return stream
|
||||
return stream.sorted(comparator)
|
||||
}
|
||||
|
||||
fun applyFiltersAndSort(
|
||||
items: List<HumanBeingProperty>,
|
||||
filters: Map<String, String>,
|
||||
comparators: List<Comparator<HumanBeingProperty>>,
|
||||
): ObservableList<HumanBeingProperty> {
|
||||
var stream = items.stream()
|
||||
|
||||
for ((key, query) in filters) {
|
||||
if (query.isNotBlank()) {
|
||||
val lower = query.trim().lowercase()
|
||||
stream = stream.filter { prop ->
|
||||
val fieldValue = when (key) {
|
||||
"name" -> prop.name.value
|
||||
"coordinates.x" -> prop.coordinateX.value.toString()
|
||||
"coordinates.y" -> prop.coordinateY.value.toString()
|
||||
"creationDate" -> prop.creationDate.value.toString()
|
||||
"realHero" -> prop.realHero.value.toString()
|
||||
"hasToothpick" -> prop.hasToothpick.value.toString()
|
||||
"impactSpeed" -> prop.impactSpeed.value.toString()
|
||||
"soundtrackName" -> prop.soundtrackName.value
|
||||
"minutesOfWaiting" -> prop.minutesOfWaiting.value.toString()
|
||||
"weaponType" -> prop.weaponType.value?.name ?: ""
|
||||
"car.cool" -> prop.carCool.value.toString()
|
||||
else -> ""
|
||||
}
|
||||
fieldValue.lowercase().contains(lower)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (comparators.isNotEmpty()) {
|
||||
val combined = comparators.reduce { a, b -> a.thenComparing(b) }
|
||||
stream = stream.sorted(combined)
|
||||
}
|
||||
|
||||
return stream.collect(Collectors.collectingAndThen(
|
||||
Collectors.toList(),
|
||||
{ FXCollections.observableArrayList(it) }
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package view
|
||||
|
||||
import controller.AuthController
|
||||
import javafx.geometry.Insets
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Scene
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.layout.HBox
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.stage.Stage
|
||||
import localization.AppLocale
|
||||
|
||||
class AuthView(private val stage: Stage, private val onSuccess: (() -> Unit)? = null) {
|
||||
|
||||
private val controller = AuthController()
|
||||
private val hostField = TextField("localhost")
|
||||
private val portField = TextField("8080")
|
||||
private val usernameField = TextField()
|
||||
private val passwordField = PasswordField()
|
||||
private val loginToggle = ToggleButton()
|
||||
private val registerToggle = ToggleButton()
|
||||
private val submitButton = Button()
|
||||
private val errorLabel = Label()
|
||||
private val localeCombo = ComboBox<LocaleDisplay>()
|
||||
|
||||
fun show() {
|
||||
updateTitle()
|
||||
stage.scene = Scene(buildUI(), 400.0, 440.0)
|
||||
stage.isResizable = false
|
||||
stage.show()
|
||||
}
|
||||
|
||||
private fun buildUI(): VBox {
|
||||
val toggleGroup = ToggleGroup()
|
||||
loginToggle.toggleGroup = toggleGroup
|
||||
registerToggle.toggleGroup = toggleGroup
|
||||
loginToggle.isSelected = true
|
||||
|
||||
AppLocale.bindLabel(loginToggle, "auth.login")
|
||||
AppLocale.bindLabel(registerToggle, "auth.register")
|
||||
|
||||
val toggleBox = HBox(10.0, loginToggle, registerToggle)
|
||||
|
||||
val usernameLabel = Label()
|
||||
AppLocale.bindLabel(usernameLabel, "auth.username")
|
||||
AppLocale.bindPrompt(usernameField, "auth.username")
|
||||
val usernameBox = VBox(3.0, usernameLabel, usernameField)
|
||||
|
||||
val passwordLabel = Label()
|
||||
AppLocale.bindLabel(passwordLabel, "auth.password")
|
||||
AppLocale.bindPrompt(passwordField, "auth.password")
|
||||
val passwordBox = VBox(3.0, passwordLabel, passwordField)
|
||||
|
||||
hostField.promptText = "localhost"
|
||||
val hostBox = VBox(3.0, Label().also { AppLocale.bindLabel(it, "auth.host") }, hostField)
|
||||
|
||||
portField.promptText = "8080"
|
||||
val portBox = VBox(3.0, Label().also { AppLocale.bindLabel(it, "auth.port") }, portField)
|
||||
|
||||
val serverBox = HBox(8.0, hostBox, portBox)
|
||||
|
||||
AppLocale.availableLocales().forEach { loc ->
|
||||
localeCombo.items.add(LocaleDisplay(AppLocale.localeDisplayName(loc), loc))
|
||||
}
|
||||
localeCombo.selectionModel.selectFirst()
|
||||
localeCombo.setOnAction {
|
||||
val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction
|
||||
AppLocale.currentLocale.set(selected.locale)
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
val localeLabel = Label()
|
||||
AppLocale.bindLabel(localeLabel, "main.locale")
|
||||
val localeBox = HBox(10.0, localeLabel, localeCombo)
|
||||
|
||||
submitButton.setOnAction { onLogin() }
|
||||
bindSubmitText()
|
||||
|
||||
toggleGroup.selectedToggleProperty().addListener { _, _, _ -> bindSubmitText() }
|
||||
|
||||
errorLabel.style = "-fx-text-fill: red;"
|
||||
|
||||
val form = VBox(12.0,
|
||||
toggleBox,
|
||||
usernameBox,
|
||||
passwordBox,
|
||||
serverBox,
|
||||
localeBox,
|
||||
submitButton,
|
||||
errorLabel,
|
||||
)
|
||||
form.alignment = Pos.CENTER
|
||||
form.padding = Insets(30.0)
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
private fun bindSubmitText() {
|
||||
val key = if (loginToggle.isSelected) "auth.submit.login" else "auth.submit.register"
|
||||
AppLocale.bind(submitButton.textProperty(), key)
|
||||
}
|
||||
|
||||
private fun updateTitle() {
|
||||
stage.title = AppLocale.get("auth.title")
|
||||
}
|
||||
|
||||
private fun onLogin() {
|
||||
val host = hostField.text.trim()
|
||||
val portText = portField.text.trim()
|
||||
val username = usernameField.text.trim()
|
||||
val password = passwordField.text.trim()
|
||||
|
||||
if (host.isEmpty() || portText.isEmpty() || username.isEmpty() || password.isEmpty()) {
|
||||
errorLabel.text = AppLocale.get("auth.error.empty")
|
||||
return
|
||||
}
|
||||
|
||||
val port = portText.toIntOrNull()
|
||||
if (port == null || port !in 1..65535) {
|
||||
errorLabel.text = AppLocale.get("auth.error.invalid.port")
|
||||
return
|
||||
}
|
||||
|
||||
val isLogin = loginToggle.isSelected
|
||||
submitButton.isDisable = true
|
||||
errorLabel.text = ""
|
||||
|
||||
controller.authenticate(host, port, username, password, isLogin) { success, message ->
|
||||
submitButton.isDisable = false
|
||||
if (success) {
|
||||
stage.close()
|
||||
onSuccess?.invoke()
|
||||
} else {
|
||||
errorLabel.text = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LocaleDisplay(val displayName: String, val locale: java.util.Locale) {
|
||||
override fun toString() = displayName
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package view
|
||||
|
||||
import javafx.animation.AnimationTimer
|
||||
import javafx.scene.canvas.Canvas
|
||||
import javafx.scene.canvas.GraphicsContext
|
||||
import javafx.scene.control.Alert
|
||||
import javafx.scene.control.ButtonType
|
||||
import javafx.scene.input.MouseEvent
|
||||
import javafx.scene.paint.Color
|
||||
import javafx.scene.text.Font
|
||||
import localization.AppLocale
|
||||
import model.HumanBeingProperty
|
||||
import model.Session
|
||||
|
||||
class CollectionCanvas : Canvas() {
|
||||
|
||||
private val gc: GraphicsContext = graphicsContext2D
|
||||
private var animationTime = 0.0
|
||||
|
||||
private var canvasWidth: Double = 0.0
|
||||
private var canvasHeight: Double = 0.0
|
||||
|
||||
private val padding = 40.0
|
||||
private val defaultRange = 200.0
|
||||
|
||||
private var scaleX: Double = 1.0
|
||||
private var scaleY: Double = 1.0
|
||||
private var offsetX: Double = 0.0
|
||||
private var offsetY: Double = 0.0
|
||||
private var minRadius = 8.0
|
||||
private var maxRadius = 35.0
|
||||
|
||||
private val renderTimer: AnimationTimer = object : AnimationTimer() {
|
||||
override fun handle(now: Long) {
|
||||
animationTime = now / 1_000_000_000.0
|
||||
render()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
widthProperty().addListener { _, _, _ -> scheduleRender() }
|
||||
heightProperty().addListener { _, _, _ -> scheduleRender() }
|
||||
addEventHandler(MouseEvent.MOUSE_CLICKED, this::onClick)
|
||||
renderTimer.start()
|
||||
Session.collection.addListener { _: javafx.collections.ListChangeListener.Change<out HumanBeingProperty>? ->
|
||||
scheduleRender()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleRender() {
|
||||
render()
|
||||
}
|
||||
|
||||
private fun render() {
|
||||
canvasWidth = width
|
||||
canvasHeight = height
|
||||
if (canvasWidth <= 0.0 || canvasHeight <= 0.0) return
|
||||
|
||||
gc.clearRect(0.0, 0.0, canvasWidth, canvasHeight)
|
||||
|
||||
drawBackground()
|
||||
|
||||
val items = Session.collection.toList()
|
||||
if (items.isEmpty()) {
|
||||
drawEmptyMessage()
|
||||
return
|
||||
}
|
||||
|
||||
computeScale(items)
|
||||
val glowIntensity = computeGlowIntensity()
|
||||
|
||||
for (item in items) {
|
||||
drawItem(item, glowIntensity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawBackground() {
|
||||
gc.fill = Color.web("#1a1a2e")
|
||||
gc.fillRect(0.0, 0.0, canvasWidth, canvasHeight)
|
||||
|
||||
gc.stroke = Color.web("#16213e")
|
||||
gc.lineWidth = 0.5
|
||||
val gridSize = 40.0
|
||||
var x = 0.0
|
||||
while (x <= canvasWidth) {
|
||||
gc.strokeLine(x, 0.0, x, canvasHeight)
|
||||
x += gridSize
|
||||
}
|
||||
var y = 0.0
|
||||
while (y <= canvasHeight) {
|
||||
gc.strokeLine(0.0, y, canvasWidth, y)
|
||||
y += gridSize
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawEmptyMessage() {
|
||||
gc.fill = Color.GRAY
|
||||
gc.font = Font(18.0)
|
||||
gc.fillText(AppLocale.get("visualization.title"), canvasWidth / 2 - 60, canvasHeight / 2)
|
||||
}
|
||||
|
||||
private fun computeScale(items: List<HumanBeingProperty>) {
|
||||
val minX = items.minOf { it.coordinateX.value }
|
||||
val maxX = items.maxOf { it.coordinateX.value }
|
||||
val minY = items.minOf { it.coordinateY.value }
|
||||
val maxY = items.maxOf { it.coordinateY.value }
|
||||
|
||||
val rangeX = if (maxX == minX) defaultRange else (maxX - minX).toDouble()
|
||||
val rangeY = if (maxY == minY) defaultRange else (maxY - minY).toDouble()
|
||||
|
||||
val availWidth = canvasWidth - 2 * padding
|
||||
val availHeight = canvasHeight - 2 * padding
|
||||
|
||||
scaleX = availWidth / rangeX
|
||||
scaleY = availHeight / rangeY
|
||||
|
||||
offsetX = padding - minX * scaleX
|
||||
offsetY = padding - minY * scaleY
|
||||
}
|
||||
|
||||
private fun computeGlowIntensity(): Double {
|
||||
return 0.4 + 0.4 * (1.0 + kotlin.math.sin(animationTime * 3.0)) / 2.0
|
||||
}
|
||||
|
||||
private fun drawItem(item: HumanBeingProperty, glowIntensity: Double) {
|
||||
val px = worldToScreenX(item.coordinateX.value.toDouble())
|
||||
val py = worldToScreenY(item.coordinateY.value.toDouble())
|
||||
val radius = mapImpactSpeed(item.impactSpeed.value)
|
||||
|
||||
val color = ownerColor(item.ownerId.value)
|
||||
val glowColor = Color(color.red, color.green, color.blue, glowIntensity * 0.5)
|
||||
|
||||
gc.save()
|
||||
|
||||
gc.setFill(glowColor)
|
||||
gc.fillOval(px - radius * 1.8, py - radius * 1.8, radius * 3.6, radius * 3.6)
|
||||
|
||||
gc.setFill(color)
|
||||
gc.fillOval(px - radius, py - radius, radius * 2, radius * 2)
|
||||
|
||||
gc.setStroke(Color.WHITE)
|
||||
gc.setLineWidth(1.5)
|
||||
gc.strokeOval(px - radius, py - radius, radius * 2, radius * 2)
|
||||
|
||||
gc.setFill(Color.WHITE)
|
||||
gc.setFont(Font(10.0))
|
||||
val label = item.name.value.take(10)
|
||||
gc.fillText(label, px - radius, py - radius - 4)
|
||||
|
||||
gc.restore()
|
||||
}
|
||||
|
||||
private fun worldToScreenX(worldX: Double): Double = worldX * scaleX + offsetX
|
||||
|
||||
private fun worldToScreenY(worldY: Double): Double = worldY * scaleY + offsetY
|
||||
|
||||
private fun screenToWorldX(screenX: Double): Double = (screenX - offsetX) / scaleX
|
||||
|
||||
private fun screenToWorldY(screenY: Double): Double = (screenY - offsetY) / scaleY
|
||||
|
||||
private fun mapImpactSpeed(speed: Double): Double {
|
||||
val items = Session.collection
|
||||
if (items.isEmpty()) return minRadius
|
||||
|
||||
val minSpeed = items.minOf { it.impactSpeed.value }
|
||||
val maxSpeed = items.maxOf { it.impactSpeed.value }
|
||||
|
||||
if (maxSpeed == minSpeed) return (minRadius + maxRadius) / 2
|
||||
|
||||
return minRadius + (speed - minSpeed) / (maxSpeed - minSpeed) * (maxRadius - minRadius)
|
||||
}
|
||||
|
||||
private fun ownerColor(ownerId: java.util.UUID): Color {
|
||||
val hue = (ownerId.hashCode() and 0x7FFFFFFF) % 360
|
||||
return Color.hsb(hue.toDouble(), 0.7, 0.9)
|
||||
}
|
||||
|
||||
private fun onClick(event: MouseEvent) {
|
||||
val mx = event.x
|
||||
val my = event.y
|
||||
|
||||
val items = Session.collection.toList()
|
||||
val hitRadius = 15.0
|
||||
|
||||
for (item in items) {
|
||||
val px = worldToScreenX(item.coordinateX.value.toDouble())
|
||||
val py = worldToScreenY(item.coordinateY.value.toDouble())
|
||||
val r = mapImpactSpeed(item.impactSpeed.value)
|
||||
val dx = mx - px
|
||||
val dy = my - py
|
||||
if (dx * dx + dy * dy <= (r + hitRadius) * (r + hitRadius)) {
|
||||
showItemInfo(item)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showItemInfo(item: HumanBeingProperty) {
|
||||
val info = buildString {
|
||||
appendLine("${AppLocale.get("info.id")}: ${item.id.value}")
|
||||
appendLine("${AppLocale.get("table.name")}: ${item.name.value}")
|
||||
appendLine("X: ${item.coordinateX.value}, Y: ${item.coordinateY.value}")
|
||||
appendLine("${AppLocale.get("table.creationDate")}: ${item.creationDate.value}")
|
||||
appendLine("${AppLocale.get("table.realHero")}: ${item.realHero.value}")
|
||||
appendLine("${AppLocale.get("table.hasToothpick")}: ${item.hasToothpick.value}")
|
||||
appendLine("${AppLocale.get("table.impactSpeed")}: ${item.impactSpeed.value}")
|
||||
appendLine("${AppLocale.get("table.soundtrackName")}: ${item.soundtrackName.value}")
|
||||
appendLine("${AppLocale.get("table.minutesOfWaiting")}: ${item.minutesOfWaiting.value}")
|
||||
appendLine("${AppLocale.get("table.weaponType")}: ${item.weaponType.value?.name ?: "—"}")
|
||||
appendLine("${AppLocale.get("table.car.cool")}: ${item.carCool.value}")
|
||||
appendLine("${AppLocale.get("info.owner")}: ${item.ownerId.value}")
|
||||
}
|
||||
val alert = Alert(Alert.AlertType.INFORMATION, info.trimEnd(), ButtonType.OK)
|
||||
alert.title = AppLocale.get("info.title")
|
||||
alert.headerText = item.name.value
|
||||
alert.show()
|
||||
}
|
||||
|
||||
override fun isResizable(): Boolean = true
|
||||
|
||||
override fun prefWidth(height: Double): Double = 400.0
|
||||
|
||||
override fun prefHeight(width: Double): Double = 400.0
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package view
|
||||
|
||||
import javafx.geometry.Insets
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.layout.GridPane
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.stage.Modality
|
||||
import javafx.stage.Stage
|
||||
import javafx.stage.Window
|
||||
import localization.AppLocale
|
||||
import models.Car
|
||||
import models.Coordinates
|
||||
import models.HumanBeing
|
||||
import models.WeaponType
|
||||
import model.Session
|
||||
import java.time.LocalDate
|
||||
|
||||
class EditDialog(private val owner: Window, private val existing: HumanBeing? = null) {
|
||||
|
||||
private val dialog = Stage()
|
||||
|
||||
private val nameField = TextField()
|
||||
private val coordXField = TextField()
|
||||
private val coordYField = TextField()
|
||||
private val realHeroCheck = CheckBox()
|
||||
private val hasToothpickCheck = CheckBox()
|
||||
private val impactSpeedField = TextField()
|
||||
private val soundtrackField = TextField()
|
||||
private val minutesField = TextField()
|
||||
private val weaponTypeCombo = ComboBox<WeaponTypeOption>()
|
||||
private val carCoolCheck = CheckBox()
|
||||
|
||||
private var result: HumanBeing? = null
|
||||
|
||||
fun showAndWait(): HumanBeing? {
|
||||
dialog.initModality(Modality.WINDOW_MODAL)
|
||||
dialog.initOwner(owner)
|
||||
dialog.scene = javafx.scene.Scene(buildUI(), 420.0, 520.0)
|
||||
dialog.isResizable = false
|
||||
|
||||
if (existing != null) populateFields(existing)
|
||||
updateTitle()
|
||||
dialog.showAndWait()
|
||||
return result
|
||||
}
|
||||
|
||||
private fun buildUI(): VBox {
|
||||
val form = GridPane()
|
||||
form.hgap = 8.0
|
||||
form.vgap = 8.0
|
||||
form.padding = Insets(16.0)
|
||||
form.alignment = Pos.CENTER_LEFT
|
||||
|
||||
var row = 0
|
||||
|
||||
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.name") }, nameField)
|
||||
row++
|
||||
|
||||
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.coordinates.x") }, coordXField)
|
||||
row++
|
||||
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.coordinates.y") }, coordYField)
|
||||
row++
|
||||
|
||||
realHeroCheck.isSelected = true
|
||||
form.addRow(row, realHeroCheck.also { AppLocale.bindLabel(it, "table.realHero") })
|
||||
row++
|
||||
|
||||
hasToothpickCheck.isSelected = true
|
||||
form.addRow(row, hasToothpickCheck.also { AppLocale.bindLabel(it, "table.hasToothpick") })
|
||||
row++
|
||||
|
||||
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.impactSpeed") }, impactSpeedField)
|
||||
row++
|
||||
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.soundtrackName") }, soundtrackField)
|
||||
row++
|
||||
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.minutesOfWaiting") }, minutesField)
|
||||
row++
|
||||
|
||||
weaponTypeCombo.items.add(WeaponTypeOption("—", null))
|
||||
WeaponType.entries.forEach { wt ->
|
||||
weaponTypeCombo.items.add(WeaponTypeOption(wt.name, wt))
|
||||
}
|
||||
weaponTypeCombo.selectionModel.selectFirst()
|
||||
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.weaponType") }, weaponTypeCombo)
|
||||
row++
|
||||
|
||||
carCoolCheck.isSelected = true
|
||||
form.addRow(row, carCoolCheck.also { AppLocale.bindLabel(it, "table.car.cool") })
|
||||
|
||||
val buttonBar = javafx.scene.layout.HBox(8.0).apply {
|
||||
alignment = Pos.CENTER_RIGHT
|
||||
padding = Insets(0.0, 16.0, 16.0, 16.0)
|
||||
children.addAll(
|
||||
Button().also {
|
||||
AppLocale.bindLabel(it, "dialog.ok")
|
||||
it.setOnAction { onOk() }
|
||||
},
|
||||
Button().also {
|
||||
AppLocale.bindLabel(it, "dialog.cancel")
|
||||
it.setOnAction { dialog.close() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return VBox(8.0, form, buttonBar)
|
||||
}
|
||||
|
||||
private fun updateTitle() {
|
||||
val key = if (existing != null) "dialog.title.edit" else "dialog.title.add"
|
||||
dialog.title = AppLocale.get(key)
|
||||
}
|
||||
|
||||
private fun populateFields(h: HumanBeing) {
|
||||
nameField.text = h.name
|
||||
coordXField.text = h.coordinates.x.toString()
|
||||
coordYField.text = h.coordinates.y.toString()
|
||||
realHeroCheck.isSelected = h.realHero
|
||||
hasToothpickCheck.isSelected = h.hasToothpick
|
||||
impactSpeedField.text = h.impactSpeed.toString()
|
||||
soundtrackField.text = h.soundtrackName
|
||||
minutesField.text = h.minutesOfWaiting.toString()
|
||||
if (h.weaponType != null) {
|
||||
val selected = weaponTypeCombo.items.find { it.type == h.weaponType }
|
||||
if (selected != null) weaponTypeCombo.selectionModel.select(selected)
|
||||
}
|
||||
carCoolCheck.isSelected = h.car.cool
|
||||
}
|
||||
|
||||
private fun onOk() {
|
||||
val name = nameField.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
showError(AppLocale.get("dialog.error.name"))
|
||||
return
|
||||
}
|
||||
|
||||
val coordX = coordXField.text.trim().toLongOrNull()
|
||||
if (coordX == null) { showError("X: " + AppLocale.get("dialog.error.number")); return }
|
||||
val coordY = coordYField.text.trim().toIntOrNull()
|
||||
if (coordY == null) { showError("Y: " + AppLocale.get("dialog.error.number")); return }
|
||||
|
||||
val impactSpeed = impactSpeedField.text.trim().toDoubleOrNull()
|
||||
if (impactSpeed == null) { showError("impactSpeed: " + AppLocale.get("dialog.error.number")); return }
|
||||
|
||||
val minutes = minutesField.text.trim().toFloatOrNull()
|
||||
if (minutes == null) { showError("minutesOfWaiting: " + AppLocale.get("dialog.error.number")); return }
|
||||
|
||||
val soundtrack = soundtrackField.text.trim()
|
||||
|
||||
result = HumanBeing(
|
||||
id = existing?.id,
|
||||
ownerId = existing?.ownerId ?: Session.userId ?: run { showError(AppLocale.get("dialog.error.no.user")); return },
|
||||
name = name,
|
||||
coordinates = Coordinates(coordX, coordY),
|
||||
creationDate = existing?.creationDate ?: LocalDate.now(),
|
||||
realHero = realHeroCheck.isSelected,
|
||||
hasToothpick = hasToothpickCheck.isSelected,
|
||||
impactSpeed = impactSpeed,
|
||||
soundtrackName = soundtrack,
|
||||
minutesOfWaiting = minutes,
|
||||
weaponType = weaponTypeCombo.selectionModel.selectedItem?.type,
|
||||
car = Car(carCoolCheck.isSelected),
|
||||
)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
val alert = Alert(Alert.AlertType.ERROR, message, ButtonType.OK)
|
||||
alert.initOwner(dialog)
|
||||
alert.show()
|
||||
}
|
||||
|
||||
private data class WeaponTypeOption(val label: String, val type: WeaponType?) {
|
||||
override fun toString() = label
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package view
|
||||
|
||||
import controller.MainController
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
|
||||
import javafx.geometry.Insets
|
||||
import javafx.geometry.Orientation
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Scene
|
||||
import view.CollectionCanvas
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.layout.*
|
||||
import javafx.stage.Stage
|
||||
import models.HumanBeing
|
||||
import localization.AppLocale
|
||||
import model.HumanBeingProperty
|
||||
import model.Session
|
||||
import java.util.Comparator
|
||||
|
||||
class MainView(private val stage: Stage) {
|
||||
|
||||
private val controller = MainController()
|
||||
|
||||
private val tableView = TableView<HumanBeingProperty>()
|
||||
private val canvas = CollectionCanvas()
|
||||
private val canvasPlaceholder = Label()
|
||||
private val usernameLabel = Label()
|
||||
private val localeCombo = ComboBox<LocaleDisplay>()
|
||||
private val addButton = Button()
|
||||
private val editButton = Button()
|
||||
private val deleteButton = Button()
|
||||
private val refreshButton = Button()
|
||||
|
||||
private val filterFields = mutableMapOf<String, TextField>()
|
||||
|
||||
private var sortColumn: String? = null
|
||||
private var sortAscending = true
|
||||
|
||||
fun show() {
|
||||
updateTitle()
|
||||
val scene = Scene(buildUI(), 1100.0, 700.0)
|
||||
stage.scene = scene
|
||||
stage.minWidth = 800.0
|
||||
stage.minHeight = 500.0
|
||||
stage.show()
|
||||
|
||||
controller.start()
|
||||
}
|
||||
|
||||
private fun buildUI(): BorderPane {
|
||||
val root = BorderPane()
|
||||
|
||||
root.top = buildToolbar()
|
||||
root.center = buildContent()
|
||||
root.bottom = buildCommandBar()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
private fun buildToolbar(): ToolBar {
|
||||
AppLocale.bindLabel(usernameLabel, "main.user")
|
||||
updateUsernameText()
|
||||
|
||||
val logoutItem = MenuItem()
|
||||
AppLocale.bind(logoutItem.textProperty(), "main.logout")
|
||||
logoutItem.setOnAction { onLogout() }
|
||||
|
||||
val userMenu = MenuButton().also { menu ->
|
||||
AppLocale.bindLabel(menu, "main.user")
|
||||
menu.items.add(logoutItem)
|
||||
}
|
||||
|
||||
AppLocale.availableLocales().forEach { loc ->
|
||||
localeCombo.items.add(LocaleDisplay(AppLocale.localeDisplayName(loc), loc))
|
||||
}
|
||||
val currentLocaleIndex = AppLocale.availableLocales().indexOf(AppLocale.currentLocale.value)
|
||||
if (currentLocaleIndex >= 0) localeCombo.selectionModel.select(currentLocaleIndex)
|
||||
localeCombo.setOnAction {
|
||||
val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction
|
||||
AppLocale.currentLocale.set(selected.locale)
|
||||
updateLocale()
|
||||
}
|
||||
|
||||
val spacer = Region()
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS)
|
||||
|
||||
val toolbar = ToolBar(
|
||||
Label().also { AppLocale.bindLabel(it, "main.user"); it.visibleProperty().bind(usernameLabel.visibleProperty()) },
|
||||
usernameLabel,
|
||||
Separator(Orientation.VERTICAL),
|
||||
Label().also { AppLocale.bindLabel(it, "main.locale") },
|
||||
localeCombo,
|
||||
spacer,
|
||||
userMenu,
|
||||
)
|
||||
return toolbar
|
||||
}
|
||||
|
||||
private fun buildContent(): Node {
|
||||
val splitPane = SplitPane()
|
||||
splitPane.orientation = Orientation.HORIZONTAL
|
||||
|
||||
splitPane.items.addAll(buildTablePane(), buildCanvasPane())
|
||||
|
||||
splitPane.setDividerPositions(0.55)
|
||||
|
||||
return splitPane
|
||||
}
|
||||
|
||||
private fun buildTablePane(): Node {
|
||||
val filterBar = GridPane()
|
||||
filterBar.hgap = 4.0
|
||||
filterBar.vgap = 2.0
|
||||
filterBar.padding = Insets(4.0)
|
||||
|
||||
val columns = listOf(
|
||||
"name" to { p: HumanBeingProperty -> p.name.value },
|
||||
"coordinates.x" to { p: HumanBeingProperty -> p.coordinateX.value.toString() },
|
||||
"coordinates.y" to { p: HumanBeingProperty -> p.coordinateY.value.toString() },
|
||||
"creationDate" to { p: HumanBeingProperty -> p.creationDate.value.toString() },
|
||||
"realHero" to { p: HumanBeingProperty -> p.realHero.value.toString() },
|
||||
"hasToothpick" to { p: HumanBeingProperty -> p.hasToothpick.value.toString() },
|
||||
"impactSpeed" to { p: HumanBeingProperty -> p.impactSpeed.value.toString() },
|
||||
"soundtrackName" to { p: HumanBeingProperty -> p.soundtrackName.value },
|
||||
"minutesOfWaiting" to { p: HumanBeingProperty -> p.minutesOfWaiting.value.toString() },
|
||||
"weaponType" to { p: HumanBeingProperty -> (p.weaponType.value?.name ?: "") },
|
||||
"car.cool" to { p: HumanBeingProperty -> p.carCool.value.toString() },
|
||||
)
|
||||
|
||||
for ((i, col) in columns.withIndex()) {
|
||||
val (key, extractor) = col
|
||||
|
||||
val headerLabel = Label()
|
||||
AppLocale.bindLabel(headerLabel, "table.$key")
|
||||
headerLabel.setOnMouseClicked {
|
||||
toggleSort(key)
|
||||
}
|
||||
headerLabel.style = "-fx-font-weight: bold; -fx-cursor: hand;"
|
||||
filterBar.add(headerLabel, i, 0)
|
||||
|
||||
val filterField = TextField()
|
||||
filterField.promptText = AppLocale.get("main.filter")
|
||||
filterField.textProperty().addListener { _, _, _ ->
|
||||
applyFilters()
|
||||
}
|
||||
filterFields[key] = filterField
|
||||
filterBar.add(filterField, i, 1)
|
||||
|
||||
val tableCol = TableColumn<HumanBeingProperty, String>()
|
||||
AppLocale.bind(tableCol.textProperty(), "table.$key")
|
||||
tableCol.cellValueFactory = javafx.util.Callback { features ->
|
||||
SimpleObjectProperty(extractor(features.value))
|
||||
}
|
||||
tableView.columns.add(tableCol)
|
||||
}
|
||||
|
||||
val tableHolder = VBox(filterBar, tableView)
|
||||
VBox.setVgrow(tableView, Priority.ALWAYS)
|
||||
|
||||
return tableHolder
|
||||
}
|
||||
|
||||
private fun buildCanvasPane(): Node {
|
||||
AppLocale.bindLabel(canvasPlaceholder, "visualization.title")
|
||||
canvasPlaceholder.alignment = Pos.CENTER
|
||||
canvasPlaceholder.style = "-fx-font-size: 16; -fx-text-fill: gray;"
|
||||
|
||||
val stackPane = StackPane(canvasPlaceholder, canvas)
|
||||
stackPane.prefWidth = 400.0
|
||||
|
||||
canvas.widthProperty().bind(stackPane.widthProperty())
|
||||
canvas.heightProperty().bind(stackPane.heightProperty())
|
||||
|
||||
return stackPane
|
||||
}
|
||||
|
||||
private fun buildCommandBar(): HBox {
|
||||
AppLocale.bindLabel(addButton, "button.add")
|
||||
AppLocale.bindLabel(editButton, "button.edit")
|
||||
AppLocale.bindLabel(deleteButton, "button.delete")
|
||||
AppLocale.bindLabel(refreshButton, "button.refresh")
|
||||
|
||||
addButton.setOnAction { onAdd() }
|
||||
editButton.setOnAction { onEdit() }
|
||||
deleteButton.setOnAction { onDelete() }
|
||||
refreshButton.setOnAction { controller.requestRefresh() }
|
||||
|
||||
val bar = HBox(8.0, addButton, editButton, deleteButton, refreshButton)
|
||||
bar.padding = Insets(8.0)
|
||||
bar.alignment = Pos.CENTER_LEFT
|
||||
return bar
|
||||
}
|
||||
|
||||
private fun toggleSort(key: String) {
|
||||
if (sortColumn == key) {
|
||||
sortAscending = !sortAscending
|
||||
} else {
|
||||
sortColumn = key
|
||||
sortAscending = true
|
||||
}
|
||||
|
||||
val comparator: Comparator<HumanBeingProperty>? = sortColumn?.let { col ->
|
||||
val asc = sortAscending
|
||||
val comp = Comparator<HumanBeingProperty> { a, b ->
|
||||
val va = extractSortValue(a, col)
|
||||
val vb = extractSortValue(b, col)
|
||||
if (asc) va.compareTo(vb) else vb.compareTo(va)
|
||||
}
|
||||
comp
|
||||
}
|
||||
|
||||
controller.updateComparators(if (comparator != null) listOf(comparator) else emptyList())
|
||||
}
|
||||
|
||||
private fun extractSortValue(prop: HumanBeingProperty, key: String): String = when (key) {
|
||||
"name" -> prop.name.value
|
||||
"coordinates.x" -> prop.coordinateX.value.toString().padStart(20, '0')
|
||||
"coordinates.y" -> prop.coordinateY.value.toString().padStart(10, '0')
|
||||
"creationDate" -> prop.creationDate.value.toString()
|
||||
"realHero" -> if (prop.realHero.value) "1" else "0"
|
||||
"hasToothpick" -> if (prop.hasToothpick.value) "1" else "0"
|
||||
"impactSpeed" -> prop.impactSpeed.value.toString().padStart(20, '0')
|
||||
"soundtrackName" -> prop.soundtrackName.value
|
||||
"minutesOfWaiting" -> prop.minutesOfWaiting.value.toString().padStart(20, '0')
|
||||
"weaponType" -> prop.weaponType.value?.name ?: ""
|
||||
"car.cool" -> if (prop.carCool.value) "1" else "0"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
private fun applyFilters() {
|
||||
val filters = filterFields.mapValues { it.value.text }
|
||||
controller.updateFilters(filters)
|
||||
}
|
||||
|
||||
init {
|
||||
tableView.items = controller.filteredList
|
||||
}
|
||||
|
||||
private fun updateUsernameText() {
|
||||
usernameLabel.text = Session.username.ifEmpty { "—" }
|
||||
}
|
||||
|
||||
private fun updateLocale() {
|
||||
updateTitle()
|
||||
updateUsernameText()
|
||||
filterFields.forEach { (_, field) ->
|
||||
field.promptText = AppLocale.get("main.filter")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTitle() {
|
||||
stage.title = AppLocale.get("main.title")
|
||||
}
|
||||
|
||||
private fun onLogout() {
|
||||
controller.stop()
|
||||
Session.client?.clearToken()
|
||||
Session.clear()
|
||||
stage.close()
|
||||
AuthView(Stage()).show()
|
||||
}
|
||||
|
||||
private fun onAdd() {
|
||||
val dialog = EditDialog(stage.scene.window)
|
||||
val humanBeing = dialog.showAndWait() ?: return
|
||||
controller.executeCommand(name = "add", humanBeing = humanBeing) { success, message ->
|
||||
if (success) controller.requestRefresh() else showError(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEdit() {
|
||||
val selected = tableView.selectionModel.selectedItem
|
||||
if (selected == null) {
|
||||
showError(AppLocale.get("error.no.selection"))
|
||||
return
|
||||
}
|
||||
if (!Session.isOwnObject(selected.ownerId.value)) {
|
||||
showError(AppLocale.get("error.not.owner"))
|
||||
return
|
||||
}
|
||||
val dialog = EditDialog(stage.scene.window, selected.toHumanBeing())
|
||||
val humanBeing = dialog.showAndWait() ?: return
|
||||
val id = selected.id.value ?: return
|
||||
controller.executeCommand(name = "update", args = listOf(id.toString()), humanBeing = humanBeing) { success, message ->
|
||||
if (success) controller.requestRefresh() else showError(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDelete() {
|
||||
val selected = tableView.selectionModel.selectedItem ?: return
|
||||
if (!Session.isOwnObject(selected.ownerId.value)) {
|
||||
showError(AppLocale.get("error.not.owner"))
|
||||
return
|
||||
}
|
||||
val confirm = Alert(Alert.AlertType.CONFIRMATION)
|
||||
confirm.title = AppLocale.get("delete.confirm.title")
|
||||
confirm.headerText = AppLocale.get("delete.confirm")
|
||||
confirm.showAndWait().ifPresent { result ->
|
||||
if (result == ButtonType.OK) {
|
||||
controller.deleteObject(selected) { success, message ->
|
||||
if (success) controller.requestRefresh() else showError(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
val alert = Alert(Alert.AlertType.ERROR, message, ButtonType.OK)
|
||||
alert.show()
|
||||
}
|
||||
|
||||
private class LocaleDisplay(val displayName: String, val locale: java.util.Locale) {
|
||||
override fun toString() = displayName
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user