Move stuff in base
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.0.0"
|
kotlin("multiplatform") version "2.0.20-Beta2"
|
||||||
id("maven-publish")
|
id("maven-publish")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "nl.astraeus"
|
group = "nl.astraeus"
|
||||||
version = "1.0.0-SNAPSHOT"
|
version = "1.1.0-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,21 +20,40 @@ kotlin {
|
|||||||
browser {
|
browser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jvm {
|
||||||
|
withJava()
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("nl.astraeus:kotlin-css-generator:1.0.7")
|
api("nl.astraeus:kotlin-css-generator:1.0.9-SNAPSHOT")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("nl.astraeus:kotlin-komponent-js:1.2.2")
|
implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsTest by getting {
|
val jsTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test-js"))
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
//base
|
||||||
|
|
||||||
|
implementation("io.undertow:undertow-core:2.3.14.Final")
|
||||||
|
implementation("io.undertow:undertow-websockets-jsr:2.3.14.Final")
|
||||||
|
implementation("org.jboss.xnio:xnio-nio:3.8.16.Final")
|
||||||
|
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.46.0.0")
|
||||||
|
implementation("com.zaxxer:HikariCP:4.0.3")
|
||||||
|
implementation("nl.astraeus:simple-jdbc-stats:1.6.1")
|
||||||
|
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
|
"@js-joda/core@3.2.0":
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
|
||||||
|
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
|
||||||
|
|
||||||
"@socket.io/component-emitter@~3.1.0":
|
"@socket.io/component-emitter@~3.1.0":
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
|
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
|
||||||
|
|||||||
53
src/commonMain/kotlin/nl/astraeus/vst/logger/Logger.kt
Normal file
53
src/commonMain/kotlin/nl/astraeus/vst/logger/Logger.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package nl.astraeus.vst.string.logger
|
||||||
|
|
||||||
|
val log = Logger
|
||||||
|
|
||||||
|
enum class LogLevel {
|
||||||
|
TRACE,
|
||||||
|
DEBUG,
|
||||||
|
INFO,
|
||||||
|
WARN,
|
||||||
|
ERROR,
|
||||||
|
FATAL
|
||||||
|
}
|
||||||
|
|
||||||
|
object Logger {
|
||||||
|
var level: LogLevel = LogLevel.INFO
|
||||||
|
|
||||||
|
fun trace(message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.TRACE.ordinal) {
|
||||||
|
println("TRACE: ${message()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun debug(message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.DEBUG.ordinal) {
|
||||||
|
println("DEBUG: ${message()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun info(message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.INFO.ordinal) {
|
||||||
|
println("INFO: ${message()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun warn(e: Throwable? = null, message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.WARN.ordinal) {
|
||||||
|
println("WARN: ${message()}")
|
||||||
|
e?.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun error(e: Throwable? = null, message: () -> String?) {
|
||||||
|
if (level.ordinal <= LogLevel.ERROR.ordinal) {
|
||||||
|
println("ERROR: ${message()}")
|
||||||
|
e?.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fatal(e: Throwable, message: () -> String?) {
|
||||||
|
println("FATAL: ${message()}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
package nl.astraeus.vst.ui.components
|
package nl.astraeus.vst.ui.components
|
||||||
|
|
||||||
import kotlinx.html.classes
|
import kotlinx.html.classes
|
||||||
import kotlinx.html.js.onMouseWheelFunction
|
import kotlinx.html.js.onWheelFunction
|
||||||
|
import kotlinx.html.org.w3c.dom.events.Event
|
||||||
import kotlinx.html.span
|
import kotlinx.html.span
|
||||||
import kotlinx.html.style
|
import kotlinx.html.style
|
||||||
import kotlinx.html.svg
|
import kotlinx.html.svg
|
||||||
import nl.astraeus.css.properties.*
|
import nl.astraeus.css.properties.Color
|
||||||
|
import nl.astraeus.css.properties.Position
|
||||||
|
import nl.astraeus.css.properties.TextAlign
|
||||||
|
import nl.astraeus.css.properties.em
|
||||||
|
import nl.astraeus.css.properties.prc
|
||||||
|
import nl.astraeus.css.properties.px
|
||||||
import nl.astraeus.css.style.cls
|
import nl.astraeus.css.style.cls
|
||||||
import nl.astraeus.komp.HtmlBuilder
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
import nl.astraeus.komp.Komponent
|
import nl.astraeus.komp.Komponent
|
||||||
@@ -14,9 +20,9 @@ import nl.astraeus.vst.ui.css.Css
|
|||||||
import nl.astraeus.vst.ui.css.Css.defineCss
|
import nl.astraeus.vst.ui.css.Css.defineCss
|
||||||
import nl.astraeus.vst.ui.css.CssId
|
import nl.astraeus.vst.ui.css.CssId
|
||||||
import nl.astraeus.vst.ui.css.CssName
|
import nl.astraeus.vst.ui.css.CssName
|
||||||
import nl.astraeus.vst.util.arc
|
import nl.astraeus.vst.ui.util.arc
|
||||||
import nl.astraeus.vst.util.height
|
import nl.astraeus.vst.ui.util.height
|
||||||
import nl.astraeus.vst.util.width
|
import nl.astraeus.vst.ui.util.width
|
||||||
import org.w3c.dom.events.MouseEvent
|
import org.w3c.dom.events.MouseEvent
|
||||||
import org.w3c.dom.events.WheelEvent
|
import org.w3c.dom.events.WheelEvent
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
@@ -77,10 +83,12 @@ open class BaseKnobComponent(
|
|||||||
width(width)
|
width(width)
|
||||||
height(height)
|
height(height)
|
||||||
|
|
||||||
|
val actToVal = actualToValue(actualValue)
|
||||||
|
console.log("actualValue", actualValue, "actToVal", actToVal)
|
||||||
val middle = (
|
val middle = (
|
||||||
((ANGLE_RANGE_DEG.toFloat() *
|
((ANGLE_RANGE_DEG.toFloat() *
|
||||||
(actualValue - actualMinimumValue)) /
|
(actToVal - minValue)) /
|
||||||
(actualMaximumValue - actualMinimumValue)
|
(maxValue - minValue)
|
||||||
+ START_ANGLE_DEG.toFloat()).toInt()
|
+ START_ANGLE_DEG.toFloat()).toInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,28 +151,7 @@ open class BaseKnobComponent(
|
|||||||
+renderedValue
|
+renderedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheelFunction = {
|
onWheelFunction = ::wheelFunction
|
||||||
if (it is WheelEvent) {
|
|
||||||
val delta = if (it.deltaY > 0) {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
-1.0
|
|
||||||
} //it.deltaY / 250.0
|
|
||||||
|
|
||||||
var newValue = actualValue - delta * step
|
|
||||||
|
|
||||||
newValue = min(newValue, actualMaximumValue)
|
|
||||||
newValue = max(newValue, actualMinimumValue)
|
|
||||||
|
|
||||||
actualValue = newValue
|
|
||||||
|
|
||||||
callback(actualToValue(newValue))
|
|
||||||
|
|
||||||
requestUpdate()
|
|
||||||
|
|
||||||
it.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* onMouseDownFunction = {
|
/* onMouseDownFunction = {
|
||||||
if (it is MouseEvent) {
|
if (it is MouseEvent) {
|
||||||
@@ -193,6 +180,31 @@ open class BaseKnobComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun wheelFunction(event: Event) {
|
||||||
|
console.log("onMouseWheelFunction", event)
|
||||||
|
if (event is WheelEvent) {
|
||||||
|
val delta = if (event.deltaY > 0) {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
-1.0
|
||||||
|
} //it.deltaY / 250.0
|
||||||
|
|
||||||
|
var newValue = actualValue - delta * step
|
||||||
|
|
||||||
|
newValue = min(newValue, actualMaximumValue)
|
||||||
|
newValue = max(newValue, actualMinimumValue)
|
||||||
|
|
||||||
|
actualValue = newValue
|
||||||
|
|
||||||
|
callback(actualToValue(newValue))
|
||||||
|
|
||||||
|
requestUpdate()
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setValueByMouseDelta(
|
private fun setValueByMouseDelta(
|
||||||
it: MouseEvent,
|
it: MouseEvent,
|
||||||
minValue: Double = 0.0,
|
minValue: Double = 0.0,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ExpKnobComponent(
|
|||||||
step: Double = 0.1,
|
step: Double = 0.1,
|
||||||
width: Int = 50,
|
width: Int = 50,
|
||||||
height: Int = 60,
|
height: Int = 60,
|
||||||
renderer: (Double) -> String = { nv -> formatDouble(nv, 2) },
|
renderer: (Double) -> String = { nv -> formatDouble(nv, 3) },
|
||||||
callback: (Double) -> Unit = {}
|
callback: (Double) -> Unit = {}
|
||||||
) : BaseKnobComponent(
|
) : BaseKnobComponent(
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package nl.astraeus.vst.ui.components
|
||||||
|
|
||||||
|
import kotlinx.html.div
|
||||||
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
|
import nl.astraeus.komp.Komponent
|
||||||
|
import nl.astraeus.vst.ui.css.Css.defineCss
|
||||||
|
import nl.astraeus.vst.ui.css.CssId
|
||||||
|
import nl.astraeus.vst.ui.css.CssName
|
||||||
|
|
||||||
|
class KeyboardInputComponent : Komponent() {
|
||||||
|
override fun HtmlBuilder.render() {
|
||||||
|
div {
|
||||||
|
+"Keyboard component"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : CssId("keyboard-input") {
|
||||||
|
object KeyboardInputCss : CssName
|
||||||
|
|
||||||
|
init {
|
||||||
|
defineCss {
|
||||||
|
select(KeyboardInputCss.cls()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package nl.astraeus.vst.util
|
package nl.astraeus.vst.ui.util
|
||||||
|
|
||||||
import kotlinx.html.SVG
|
import kotlinx.html.SVG
|
||||||
import kotlinx.html.unsafe
|
import kotlinx.html.unsafe
|
||||||
98
src/jsMain/kotlin/nl/astraeus/vst/ui/view/BaseVstView.kt
Normal file
98
src/jsMain/kotlin/nl/astraeus/vst/ui/view/BaseVstView.kt
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package nl.astraeus.vst.ui.view
|
||||||
|
|
||||||
|
import kotlinx.html.div
|
||||||
|
import kotlinx.html.js.onClickFunction
|
||||||
|
import kotlinx.html.org.w3c.dom.events.Event
|
||||||
|
import nl.astraeus.css.properties.Display
|
||||||
|
import nl.astraeus.css.properties.FlexDirection
|
||||||
|
import nl.astraeus.css.properties.Position
|
||||||
|
import nl.astraeus.css.properties.Transform
|
||||||
|
import nl.astraeus.css.properties.hsla
|
||||||
|
import nl.astraeus.css.properties.prc
|
||||||
|
import nl.astraeus.css.properties.px
|
||||||
|
import nl.astraeus.css.properties.rem
|
||||||
|
import nl.astraeus.css.properties.vh
|
||||||
|
import nl.astraeus.css.properties.vw
|
||||||
|
import nl.astraeus.css.style.cls
|
||||||
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
|
import nl.astraeus.komp.Komponent
|
||||||
|
import nl.astraeus.vst.ui.css.Css
|
||||||
|
import nl.astraeus.vst.ui.css.Css.defineCss
|
||||||
|
import nl.astraeus.vst.ui.css.CssId
|
||||||
|
import nl.astraeus.vst.ui.css.CssName
|
||||||
|
|
||||||
|
class BaseVstView(
|
||||||
|
val title: String,
|
||||||
|
val vstView: Komponent,
|
||||||
|
val audioStart: (e: Event) -> Unit
|
||||||
|
) : Komponent() {
|
||||||
|
var started = false
|
||||||
|
|
||||||
|
override fun HtmlBuilder.render() {
|
||||||
|
div(BaseVstCss.name) {
|
||||||
|
if (!started) {
|
||||||
|
div(BaseVstCss.StartSplashCss.name) {
|
||||||
|
div(BaseVstCss.StartBoxCss.name) {
|
||||||
|
div(BaseVstCss.StartButtonCss.name) {
|
||||||
|
+"START"
|
||||||
|
onClickFunction = { event ->
|
||||||
|
audioStart(event)
|
||||||
|
started = true
|
||||||
|
requestUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
include(vstView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BaseVstCss : CssId("base-vst-view") {
|
||||||
|
object BaseVstCss : CssName
|
||||||
|
object StartSplashCss : CssName
|
||||||
|
object StartBoxCss : CssName
|
||||||
|
object StartButtonCss : CssName
|
||||||
|
|
||||||
|
init {
|
||||||
|
defineCss {
|
||||||
|
select(BaseVstCss.cls()) {
|
||||||
|
display(Display.flex)
|
||||||
|
flexDirection(FlexDirection.column)
|
||||||
|
}
|
||||||
|
|
||||||
|
select(cls(StartSplashCss)) {
|
||||||
|
position(Position.fixed)
|
||||||
|
left(0.px)
|
||||||
|
top(0.px)
|
||||||
|
width(100.vw)
|
||||||
|
height(100.vh)
|
||||||
|
zIndex(100)
|
||||||
|
backgroundColor(hsla(32, 0, 5, 0.65))
|
||||||
|
|
||||||
|
select(cls(StartBoxCss)) {
|
||||||
|
position(Position.relative)
|
||||||
|
left(25.vw)
|
||||||
|
top(25.vh)
|
||||||
|
width(50.vw)
|
||||||
|
height(50.vh)
|
||||||
|
backgroundColor(hsla(239, 50, 10, 1.0))
|
||||||
|
borderColor(Css.currentStyle.mainFontColor)
|
||||||
|
borderWidth(2.px)
|
||||||
|
|
||||||
|
select(cls(StartButtonCss)) {
|
||||||
|
position(Position.absolute)
|
||||||
|
left(50.prc)
|
||||||
|
top(50.prc)
|
||||||
|
transform(Transform("translate(-50%, -50%)"))
|
||||||
|
padding(1.rem)
|
||||||
|
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||||
|
cursor("pointer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
src/jvmMain/kotlin/nl/astraeus/vst/base/Settings.kt
Normal file
44
src/jvmMain/kotlin/nl/astraeus/vst/base/Settings.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package nl.astraeus.vst.base
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object Settings {
|
||||||
|
var port = 9004
|
||||||
|
var connectionTimeout = 30000
|
||||||
|
var jdbcStatsPort = 6001
|
||||||
|
|
||||||
|
var jdbcDriver = "nl.astraeus.jdbc.Driver"
|
||||||
|
val jdbcConnectionUrl
|
||||||
|
get() = "jdbc:stat:webServerPort=$jdbcStatsPort:jdbc:sqlite:data/vst.db"
|
||||||
|
var jdbcUser = "sa"
|
||||||
|
var jdbcPassword = ""
|
||||||
|
|
||||||
|
fun getPropertiesFromFile(filename: String): Properties? {
|
||||||
|
val propertiesFile = File(filename)
|
||||||
|
return if (propertiesFile.exists()) {
|
||||||
|
val properties = Properties()
|
||||||
|
FileInputStream(propertiesFile).use {
|
||||||
|
properties.load(it)
|
||||||
|
}
|
||||||
|
properties
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readProperties(args: Array<String>) {
|
||||||
|
val filename = if (args.isNotEmpty()) args[0] else "srp.properties"
|
||||||
|
val properties =
|
||||||
|
getPropertiesFromFile(filename) ?: return // return if properties couldn't be loaded
|
||||||
|
|
||||||
|
port = properties.getProperty("port", port.toString()).toInt()
|
||||||
|
jdbcStatsPort = properties.getProperty("jdbcStatsPort", jdbcStatsPort.toString()).toInt()
|
||||||
|
connectionTimeout =
|
||||||
|
properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
|
||||||
|
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)
|
||||||
|
jdbcUser = properties.getProperty("jdbcUser", jdbcUser)
|
||||||
|
jdbcPassword = properties.getProperty("jdbcPassword", jdbcPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
170
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt
Normal file
170
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import nl.astraeus.vst.string.logger.log
|
||||||
|
import java.sql.PreparedStatement
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import java.sql.Timestamp
|
||||||
|
|
||||||
|
fun Instant.toSqlTimestamp() = Timestamp(this.toEpochMilliseconds())
|
||||||
|
fun Timestamp.toDateTimeInstant() = Instant.fromEpochMilliseconds(this.time)
|
||||||
|
|
||||||
|
data class SqlStatement<T : Entity>(
|
||||||
|
val sql: String,
|
||||||
|
val prepareParameters: T.(PreparedStatement) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SqlQuery<T : Entity>(
|
||||||
|
val sql: String,
|
||||||
|
val resultMapper: (ResultSet) -> T
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract class QueryProvider<T : Entity> {
|
||||||
|
abstract val tableName: String
|
||||||
|
open val idQuery: String
|
||||||
|
get() = "SELECT * FROM $tableName WHERE ID = ?"
|
||||||
|
abstract val resultSetMapper: (ResultSet) -> T
|
||||||
|
open val find: SqlQuery<T>
|
||||||
|
get() = SqlQuery(
|
||||||
|
idQuery,
|
||||||
|
resultSetMapper
|
||||||
|
)
|
||||||
|
abstract val insert: SqlStatement<T>
|
||||||
|
abstract val update: SqlStatement<T>
|
||||||
|
open val delete: SqlStatement<T>
|
||||||
|
get() = SqlStatement(
|
||||||
|
"DELETE FROM $tableName WHERE ID = ?"
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, getPK()[0] as Long)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseDao<T : Entity> {
|
||||||
|
abstract val queryProvider: QueryProvider<T>
|
||||||
|
open val autogeneratedPrimaryKey: Boolean = true
|
||||||
|
|
||||||
|
open fun insert(entity: T) {
|
||||||
|
executeInsert(entity, "insert", queryProvider.insert)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun update(entity: T): Int = executeUpdate(
|
||||||
|
entity,
|
||||||
|
"update",
|
||||||
|
queryProvider.update,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
open fun upsert(entity: T) {
|
||||||
|
if ((entity.getPK()[0] as Long) == 0L) {
|
||||||
|
insert(entity)
|
||||||
|
} else {
|
||||||
|
update(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun delete(entity: T) {
|
||||||
|
executeUpdate(entity, "delete", queryProvider.delete, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun find(
|
||||||
|
id: Long
|
||||||
|
): T? {
|
||||||
|
return executeQuery(
|
||||||
|
"find",
|
||||||
|
queryProvider.find
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, id)
|
||||||
|
}.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeSQLUpdate(
|
||||||
|
sql: String,
|
||||||
|
parameterSetter: (PreparedStatement) -> Unit
|
||||||
|
): Int {
|
||||||
|
return Database.transaction { con ->
|
||||||
|
con.prepareStatement(sql).use { ps ->
|
||||||
|
parameterSetter(ps)
|
||||||
|
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeQuery(
|
||||||
|
label: String,
|
||||||
|
statement: SqlQuery<T>,
|
||||||
|
prepareParameters: (PreparedStatement) -> Unit,
|
||||||
|
): List<T> {
|
||||||
|
return Database.transaction { con ->
|
||||||
|
log.debug { "Executing query [$label] - [${statement.sql}]" }
|
||||||
|
val result = mutableListOf<T>()
|
||||||
|
|
||||||
|
con.prepareStatement(statement.sql)?.use { ps ->
|
||||||
|
prepareParameters(ps)
|
||||||
|
|
||||||
|
val rs = ps.executeQuery()
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
result.add(statement.resultMapper(rs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeInsert(
|
||||||
|
entity: T,
|
||||||
|
label: String,
|
||||||
|
statement: SqlStatement<T>,
|
||||||
|
checkSingleRow: Boolean = false
|
||||||
|
) {
|
||||||
|
Database.transaction { con ->
|
||||||
|
log.debug { "Executing insert [$label] - [${statement.sql}] - [$entity]" }
|
||||||
|
con.prepareStatement(statement.sql)?.use { ps ->
|
||||||
|
statement.prepareParameters(entity, ps)
|
||||||
|
|
||||||
|
val rows = if (checkSingleRow) {
|
||||||
|
ps.execute()
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autogeneratedPrimaryKey) {
|
||||||
|
val keyResult = ps.generatedKeys
|
||||||
|
if (keyResult.next()) {
|
||||||
|
entity.setPK(arrayOf(keyResult.getLong(1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check(rows == 1) {
|
||||||
|
"Statement [$label] affected more than 1 row! [${statement.sql}]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun executeUpdate(
|
||||||
|
entity: T,
|
||||||
|
label: String,
|
||||||
|
statement: SqlStatement<T>,
|
||||||
|
checkSingleRow: Boolean = false
|
||||||
|
): Int = Database.transaction { con ->
|
||||||
|
var rows = 1
|
||||||
|
|
||||||
|
log.debug { "Executing update [$label] - [${statement.sql}] - [$entity]" }
|
||||||
|
con.prepareStatement(statement.sql)?.use { ps ->
|
||||||
|
statement.prepareParameters(entity, ps)
|
||||||
|
|
||||||
|
rows = ps.executeUpdate()
|
||||||
|
|
||||||
|
check(checkSingleRow || rows == 1) {
|
||||||
|
"Statement [$label] affected more than 1 row! [${statement.sql}]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
84
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt
Normal file
84
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig
|
||||||
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
|
import nl.astraeus.vst.base.Settings
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
val DATABASE_MIGRATIONS = arrayOf<Migration>(
|
||||||
|
Migration.Query(
|
||||||
|
"""
|
||||||
|
CREATE TABLE DATABASE_VERSION (
|
||||||
|
ID INTEGER PRIMARY KEY,
|
||||||
|
QUERY TEXT,
|
||||||
|
EXECUTED TIMESTAMP
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
Migration.Query(PATCH_CREATE_QUERY),
|
||||||
|
)
|
||||||
|
|
||||||
|
object Database {
|
||||||
|
|
||||||
|
private var ds: HikariDataSource? = null
|
||||||
|
private val currentConnection = ThreadLocal<Connection>()
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
Class.forName("nl.astraeus.jdbc.Driver")
|
||||||
|
|
||||||
|
val properties = Properties()
|
||||||
|
properties["journal_mode"] = "WAL"
|
||||||
|
|
||||||
|
val config = HikariConfig().apply {
|
||||||
|
driverClassName = Settings.jdbcDriver
|
||||||
|
jdbcUrl = Settings.jdbcConnectionUrl
|
||||||
|
username = Settings.jdbcUser
|
||||||
|
password = Settings.jdbcPassword
|
||||||
|
maximumPoolSize = 25
|
||||||
|
isAutoCommit = false
|
||||||
|
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
config.dataSourceProperties = properties
|
||||||
|
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSize", "250")
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
|
||||||
|
|
||||||
|
ds = HikariDataSource(config)
|
||||||
|
Migrations.databaseVersionTableCreated = AtomicBoolean(false)
|
||||||
|
Migrations.updateDatabaseIfNeeded(DATABASE_MIGRATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConnection() = ds?.connection ?: error("Database has not been initialized!")
|
||||||
|
|
||||||
|
fun <T> transaction(
|
||||||
|
block: (Connection) -> T
|
||||||
|
): T {
|
||||||
|
val hasConnection = currentConnection.get() != null
|
||||||
|
|
||||||
|
if (!hasConnection) {
|
||||||
|
currentConnection.set(getConnection())
|
||||||
|
}
|
||||||
|
|
||||||
|
val connection = currentConnection.get()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = block(connection)
|
||||||
|
|
||||||
|
if (!hasConnection) {
|
||||||
|
connection.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
if (!hasConnection) {
|
||||||
|
connection.close()
|
||||||
|
currentConnection.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt
Normal file
16
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
interface Entity {
|
||||||
|
fun getPK(): Array<Any>
|
||||||
|
fun setPK(pks: Array<Any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityId : Entity {
|
||||||
|
var id: Long
|
||||||
|
|
||||||
|
override fun getPK(): Array<Any> = arrayOf(id)
|
||||||
|
|
||||||
|
override fun setPK(pks: Array<Any>) {
|
||||||
|
id = pks[0] as Long
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt
Normal file
97
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
import nl.astraeus.vst.base.db.Database.transaction
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.sql.SQLException
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
sealed class Migration {
|
||||||
|
class Query(
|
||||||
|
val query: String
|
||||||
|
) : Migration() {
|
||||||
|
override fun toString(): String {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Code(
|
||||||
|
val code: (Connection) -> Unit
|
||||||
|
) : Migration() {
|
||||||
|
override fun toString(): String {
|
||||||
|
return code.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Migrations {
|
||||||
|
var databaseVersionTableCreated = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun updateDatabaseIfNeeded(
|
||||||
|
migrations: Array<Migration>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Database.transaction { con ->
|
||||||
|
con.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT MAX(ID) FROM DATABASE_VERSION
|
||||||
|
""".trimIndent()
|
||||||
|
).use { ps ->
|
||||||
|
ps.executeQuery().use { rs ->
|
||||||
|
databaseVersionTableCreated.compareAndSet(false, true)
|
||||||
|
|
||||||
|
if (rs.next()) {
|
||||||
|
val maxId = rs.getInt(1)
|
||||||
|
|
||||||
|
for (index in maxId + 1..<migrations.size) {
|
||||||
|
executeMigration(index, migrations[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SQLException) {
|
||||||
|
if (databaseVersionTableCreated.compareAndSet(false, true)) {
|
||||||
|
executeMigration(0, migrations[0])
|
||||||
|
updateDatabaseIfNeeded(migrations)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeMigration(
|
||||||
|
index: Int,
|
||||||
|
migration: Migration
|
||||||
|
) {
|
||||||
|
transaction { con ->
|
||||||
|
/* log.debug {
|
||||||
|
"Executing migration $index - [${migration}]"
|
||||||
|
}*/
|
||||||
|
val description = when (migration) {
|
||||||
|
is Migration.Query -> {
|
||||||
|
@Suppress("SqlSourceToSinkFlow")
|
||||||
|
con.prepareStatement(migration.query).use { ps ->
|
||||||
|
ps.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.query
|
||||||
|
}
|
||||||
|
|
||||||
|
is Migration.Code -> {
|
||||||
|
migration.code(con)
|
||||||
|
|
||||||
|
migration.code.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
con.prepareStatement("INSERT INTO DATABASE_VERSION VALUES (?, ?, ?)").use { ps ->
|
||||||
|
ps.setInt(1, index)
|
||||||
|
ps.setString(2, description)
|
||||||
|
ps.setTimestamp(3, Timestamp(System.currentTimeMillis()))
|
||||||
|
|
||||||
|
ps.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt
Normal file
31
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
object PatchDao : BaseDao<PatchEntity>() {
|
||||||
|
|
||||||
|
override val queryProvider: QueryProvider<PatchEntity>
|
||||||
|
get() = PatchEntityQueryProvider
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
patchId: String,
|
||||||
|
patch: String
|
||||||
|
): PatchEntity {
|
||||||
|
val result = PatchEntity(
|
||||||
|
0,
|
||||||
|
patchId,
|
||||||
|
patch
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findById(patchId: String): PatchEntity? = executeQuery(
|
||||||
|
"findById",
|
||||||
|
SqlQuery(
|
||||||
|
"SELECT * FROM ${queryProvider.tableName} WHERE PATCH_ID = ?",
|
||||||
|
queryProvider.resultSetMapper
|
||||||
|
)
|
||||||
|
) { ps ->
|
||||||
|
ps.setString(1, patchId)
|
||||||
|
}.firstOrNull()
|
||||||
|
|
||||||
|
}
|
||||||
12
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt
Normal file
12
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
data class PatchEntity(
|
||||||
|
override var id: Long,
|
||||||
|
var patchId: String,
|
||||||
|
var patch: String,
|
||||||
|
var created: Instant = Clock.System.now(),
|
||||||
|
var updated: Instant = Clock.System.now(),
|
||||||
|
) : EntityId
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package nl.astraeus.vst.base.db
|
||||||
|
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import java.sql.Types
|
||||||
|
|
||||||
|
val PATCH_CREATE_QUERY = """
|
||||||
|
CREATE TABLE INSTRUMENTS (
|
||||||
|
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
PATCH_ID TEXT,
|
||||||
|
PATCH TEXT,
|
||||||
|
CREATED TIMESTAMP,
|
||||||
|
UPDATED TIMESTAMP
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
object PatchEntityQueryProvider : QueryProvider<PatchEntity>() {
|
||||||
|
override val tableName: String
|
||||||
|
get() = "INSTRUMENTS"
|
||||||
|
override val resultSetMapper: (ResultSet) -> PatchEntity
|
||||||
|
get() = { rs ->
|
||||||
|
PatchEntity(
|
||||||
|
rs.getLong(1),
|
||||||
|
rs.getString(2),
|
||||||
|
rs.getString(3),
|
||||||
|
rs.getTimestamp(4).toDateTimeInstant(),
|
||||||
|
rs.getTimestamp(5).toDateTimeInstant()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val insert: SqlStatement<PatchEntity>
|
||||||
|
get() = SqlStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO $tableName (
|
||||||
|
ID,
|
||||||
|
PATCH_ID,
|
||||||
|
PATCH,
|
||||||
|
CREATED,
|
||||||
|
UPDATED
|
||||||
|
) VALUES (
|
||||||
|
?,?,?,?,?
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
) { ps ->
|
||||||
|
ps.setNull(1, Types.BIGINT)
|
||||||
|
ps.setString(2, patchId)
|
||||||
|
ps.setString(3, patch)
|
||||||
|
ps.setTimestamp(4, created.toSqlTimestamp())
|
||||||
|
ps.setTimestamp(5, updated.toSqlTimestamp())
|
||||||
|
}
|
||||||
|
override val update: SqlStatement<PatchEntity>
|
||||||
|
get() = SqlStatement(
|
||||||
|
"""
|
||||||
|
UPDATE $tableName
|
||||||
|
SET PATCH_ID = ?,
|
||||||
|
PATCH = ?,
|
||||||
|
UPDATED = ?
|
||||||
|
WHERE ID = ?
|
||||||
|
""".trimIndent()
|
||||||
|
) { ps ->
|
||||||
|
ps.setString(1, patchId)
|
||||||
|
ps.setString(2, patch)
|
||||||
|
ps.setTimestamp(3, updated.toSqlTimestamp())
|
||||||
|
ps.setLong(4, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/jvmMain/kotlin/nl/astraeus/vst/base/web/GenerateId.kt
Normal file
16
src/jvmMain/kotlin/nl/astraeus/vst/base/web/GenerateId.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package nl.astraeus.vst.base.web
|
||||||
|
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
val idChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
val random = SecureRandom()
|
||||||
|
|
||||||
|
fun generateId(): String {
|
||||||
|
val id = StringBuilder()
|
||||||
|
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
id.append(idChars[random.nextInt(idChars.length)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return id.toString()
|
||||||
|
}
|
||||||
46
src/jvmMain/kotlin/nl/astraeus/vst/base/web/Index.kt
Normal file
46
src/jvmMain/kotlin/nl/astraeus/vst/base/web/Index.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package nl.astraeus.vst.base.web
|
||||||
|
|
||||||
|
import kotlinx.html.body
|
||||||
|
import kotlinx.html.head
|
||||||
|
import kotlinx.html.html
|
||||||
|
import kotlinx.html.meta
|
||||||
|
import kotlinx.html.script
|
||||||
|
import kotlinx.html.stream.appendHTML
|
||||||
|
import kotlinx.html.title
|
||||||
|
|
||||||
|
fun generateIndex(
|
||||||
|
title: String,
|
||||||
|
script: String,
|
||||||
|
patch: String?,
|
||||||
|
): String {
|
||||||
|
val result = StringBuilder();
|
||||||
|
|
||||||
|
if (patch == null) {
|
||||||
|
result.appendHTML(true).html {
|
||||||
|
head {
|
||||||
|
title { +title }
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
script {
|
||||||
|
type = "application/javascript"
|
||||||
|
src = script
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.appendHTML(true).html {
|
||||||
|
head {
|
||||||
|
title { +"VST Chip" }
|
||||||
|
meta {
|
||||||
|
httpEquiv = "refresh"
|
||||||
|
content = "0; url=/patch/$patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
+"Redirecting to patch $patch..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
134
src/jvmMain/kotlin/nl/astraeus/vst/base/web/RequestHandler.kt
Normal file
134
src/jvmMain/kotlin/nl/astraeus/vst/base/web/RequestHandler.kt
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package nl.astraeus.vst.base.web
|
||||||
|
|
||||||
|
import io.undertow.Handlers.websocket
|
||||||
|
import io.undertow.server.HttpHandler
|
||||||
|
import io.undertow.server.HttpServerExchange
|
||||||
|
import io.undertow.server.handlers.PathHandler
|
||||||
|
import io.undertow.server.handlers.resource.PathResourceManager
|
||||||
|
import io.undertow.server.handlers.resource.ResourceHandler
|
||||||
|
import io.undertow.server.session.Session
|
||||||
|
import io.undertow.server.session.SessionConfig
|
||||||
|
import io.undertow.server.session.SessionManager
|
||||||
|
import io.undertow.websockets.WebSocketConnectionCallback
|
||||||
|
import io.undertow.websockets.core.AbstractReceiveListener
|
||||||
|
import io.undertow.websockets.core.BufferedBinaryMessage
|
||||||
|
import io.undertow.websockets.core.BufferedTextMessage
|
||||||
|
import io.undertow.websockets.core.WebSocketChannel
|
||||||
|
import io.undertow.websockets.core.WebSockets
|
||||||
|
import io.undertow.websockets.spi.WebSocketHttpExchange
|
||||||
|
import nl.astraeus.vst.base.db.Database
|
||||||
|
import nl.astraeus.vst.base.db.PatchDao
|
||||||
|
import nl.astraeus.vst.base.db.PatchEntity
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
class WebsocketHandler(
|
||||||
|
val session: Session?
|
||||||
|
) : AbstractReceiveListener(), WebSocketConnectionCallback {
|
||||||
|
|
||||||
|
override fun onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel) {
|
||||||
|
channel.receiveSetter.set(this)
|
||||||
|
channel.resumeReceives()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) {
|
||||||
|
val vstSession = session?.getAttribute("html-session") as? VstSession
|
||||||
|
|
||||||
|
val data = message.data
|
||||||
|
val commandLength = data.indexOf('\n')
|
||||||
|
if (commandLength > 0) {
|
||||||
|
val command = data.substring(0, commandLength)
|
||||||
|
val value = data.substring(commandLength + 1)
|
||||||
|
|
||||||
|
when (command) {
|
||||||
|
"SAVE" -> {
|
||||||
|
val patchId = vstSession?.patchId
|
||||||
|
if (patchId != null) {
|
||||||
|
Database.transaction {
|
||||||
|
val patchEntity = PatchDao.findById(patchId)
|
||||||
|
|
||||||
|
if (patchEntity != null) {
|
||||||
|
PatchDao.update(patchEntity.copy(patch = value))
|
||||||
|
} else {
|
||||||
|
PatchDao.insert(PatchEntity(0, patchId, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WebSockets.sendText("SAVED\n$patchId", channel, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"LOAD" -> {
|
||||||
|
val patchId = vstSession?.patchId
|
||||||
|
if (patchId != null) {
|
||||||
|
Database.transaction {
|
||||||
|
val patchEntity = PatchDao.findById(patchId)
|
||||||
|
|
||||||
|
if (patchEntity != null) {
|
||||||
|
WebSockets.sendText("LOAD\n${patchEntity.patch}", channel, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullBinaryMessage(channel: WebSocketChannel?, message: BufferedBinaryMessage?) {
|
||||||
|
// do nothing yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object WebsocketConnectHandler : HttpHandler {
|
||||||
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
|
val sessionManager = exchange.getAttachment(SessionManager.ATTACHMENT_KEY)
|
||||||
|
val sessionConfig = exchange.getAttachment(SessionConfig.ATTACHMENT_KEY)
|
||||||
|
|
||||||
|
val httpSession: Session? = sessionManager.getSession(exchange, sessionConfig)
|
||||||
|
|
||||||
|
websocket(WebsocketHandler(httpSession)).handleRequest(exchange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatchHandler(
|
||||||
|
val title: String,
|
||||||
|
val scriptName: String,
|
||||||
|
) : HttpHandler {
|
||||||
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
|
if (exchange.requestPath.startsWith("/patch/")) {
|
||||||
|
val patchId = exchange.requestPath.substring(7)
|
||||||
|
val sessionManager = exchange.getAttachment(SessionManager.ATTACHMENT_KEY)
|
||||||
|
val sessionConfig = exchange.getAttachment(SessionConfig.ATTACHMENT_KEY)
|
||||||
|
var httpSession: Session? = sessionManager.getSession(exchange, sessionConfig)
|
||||||
|
|
||||||
|
if (httpSession == null) {
|
||||||
|
httpSession = sessionManager.createSession(exchange, sessionConfig)
|
||||||
|
}
|
||||||
|
httpSession?.setAttribute("html-session", VstSession(patchId))
|
||||||
|
|
||||||
|
exchange.responseSender.send(generateIndex(title, scriptName, null))
|
||||||
|
} else {
|
||||||
|
val patchId = generateId()
|
||||||
|
|
||||||
|
exchange.responseSender.send(generateIndex(title, scriptName, patchId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestHandler(
|
||||||
|
title: String,
|
||||||
|
scriptName: String,
|
||||||
|
) : HttpHandler {
|
||||||
|
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
|
||||||
|
val pathHandler = PathHandler(resourceHandler)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val patchHandler = PatchHandler(title, scriptName)
|
||||||
|
pathHandler.addExactPath("/", patchHandler)
|
||||||
|
pathHandler.addExactPath("/index.html", patchHandler)
|
||||||
|
pathHandler.addPrefixPath("/patch", patchHandler)
|
||||||
|
pathHandler.addExactPath("/ws", WebsocketConnectHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRequest(exchange: HttpServerExchange) {
|
||||||
|
pathHandler.handleRequest(exchange)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/jvmMain/kotlin/nl/astraeus/vst/base/web/Session.kt
Normal file
5
src/jvmMain/kotlin/nl/astraeus/vst/base/web/Session.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package nl.astraeus.vst.base.web
|
||||||
|
|
||||||
|
class VstSession(
|
||||||
|
val patchId: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package nl.astraeus.vst.base.web
|
||||||
|
|
||||||
|
import io.undertow.Undertow
|
||||||
|
import io.undertow.UndertowOptions
|
||||||
|
import io.undertow.server.session.InMemorySessionManager
|
||||||
|
import io.undertow.server.session.SessionAttachmentHandler
|
||||||
|
import io.undertow.server.session.SessionCookieConfig
|
||||||
|
import nl.astraeus.vst.base.Settings
|
||||||
|
|
||||||
|
object UndertowServer {
|
||||||
|
var server: Undertow? = null
|
||||||
|
|
||||||
|
fun start(
|
||||||
|
title: String,
|
||||||
|
scriptName: String,
|
||||||
|
) {
|
||||||
|
val sessionHandler = SessionAttachmentHandler(
|
||||||
|
InMemorySessionManager("vst-session-manager"),
|
||||||
|
SessionCookieConfig()
|
||||||
|
)
|
||||||
|
sessionHandler.setNext(RequestHandler(title, scriptName))
|
||||||
|
|
||||||
|
val server = Undertow.builder()
|
||||||
|
.addHttpListener(Settings.port, "localhost")
|
||||||
|
.setIoThreads(4)
|
||||||
|
.setHandler(sessionHandler)
|
||||||
|
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
println("Starting server at port ${Settings.port}...")
|
||||||
|
|
||||||
|
server?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
package nl.astraeus.vst.ui.components
|
||||||
|
|
||||||
|
import kotlinx.html.classes
|
||||||
|
import kotlinx.html.js.onMouseWheelFunction
|
||||||
|
import kotlinx.html.js.onWheelFunction
|
||||||
|
import kotlinx.html.org.w3c.dom.events.Event
|
||||||
|
import kotlinx.html.span
|
||||||
|
import kotlinx.html.style
|
||||||
|
import kotlinx.html.svg
|
||||||
|
import nl.astraeus.css.properties.*
|
||||||
|
import nl.astraeus.css.style.cls
|
||||||
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
|
import nl.astraeus.komp.Komponent
|
||||||
|
import nl.astraeus.vst.ui.css.ActiveCls
|
||||||
|
import nl.astraeus.vst.ui.css.Css
|
||||||
|
import nl.astraeus.vst.ui.css.Css.defineCss
|
||||||
|
import nl.astraeus.vst.ui.css.CssId
|
||||||
|
import nl.astraeus.vst.ui.css.CssName
|
||||||
|
import nl.astraeus.vst.util.arc
|
||||||
|
import nl.astraeus.vst.util.height
|
||||||
|
import nl.astraeus.vst.util.width
|
||||||
|
import org.w3c.dom.events.MouseEvent
|
||||||
|
import org.w3c.dom.events.WheelEvent
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User: rnentjes
|
||||||
|
* Date: 26-11-17
|
||||||
|
* Time: 16:52
|
||||||
|
*/
|
||||||
|
|
||||||
|
private const val START_ANGLE_DEG = 230
|
||||||
|
private const val END_ANGLE_DEG = 130
|
||||||
|
private const val ANGLE_RANGE_DEG = 260
|
||||||
|
|
||||||
|
private const val START_ANGLE = PI * START_ANGLE_DEG / 180.toFloat() - PI / 2
|
||||||
|
private const val END_ANGLE = PI * END_ANGLE_DEG / 180.toFloat() - PI / 2
|
||||||
|
private const val ANGLE_RANGE = PI * ANGLE_RANGE_DEG / 180.toFloat()
|
||||||
|
|
||||||
|
open class BaseKnobComponent(
|
||||||
|
val value: Double,
|
||||||
|
val label: String,
|
||||||
|
val minValue: Double,
|
||||||
|
val maxValue: Double,
|
||||||
|
val step: Double,
|
||||||
|
val discrete: Boolean,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val pixelStep: Double,
|
||||||
|
val valueToActual: (Double) -> Double,
|
||||||
|
val actualToValue: (Double) -> Double,
|
||||||
|
val renderer: (Double) -> String,
|
||||||
|
val callback: (Double) -> Unit
|
||||||
|
) : Komponent() {
|
||||||
|
val actualMinimumValue = valueToActual(minValue)
|
||||||
|
val actualMaximumValue = valueToActual(maxValue)
|
||||||
|
var actualValue = valueToActual(value)
|
||||||
|
|
||||||
|
var activated = false
|
||||||
|
var mouseX = 0.0
|
||||||
|
var mouseY = 0.0
|
||||||
|
var startValue = 0.0
|
||||||
|
|
||||||
|
private fun getMiddleX() = width / 2
|
||||||
|
private fun getMiddleY() = ((height - 16) / 2) + 16
|
||||||
|
private fun getRadius() = min(getMiddleX(), getMiddleY() - 16) - 5
|
||||||
|
|
||||||
|
override fun HtmlBuilder.render() {
|
||||||
|
span(KnobCls.name) {
|
||||||
|
style = "width: ${width}px; height: ${height}px"
|
||||||
|
|
||||||
|
svg(KnobSvgCls.name) {
|
||||||
|
if (activated) {
|
||||||
|
classes = classes + ActiveCls.name
|
||||||
|
}
|
||||||
|
|
||||||
|
width(width)
|
||||||
|
height(height)
|
||||||
|
|
||||||
|
val actToVal = actualToValue(actualValue)
|
||||||
|
println("actualValue: $actualValue, actToVal: $actToVal")
|
||||||
|
val middle = (
|
||||||
|
((ANGLE_RANGE_DEG.toFloat() *
|
||||||
|
(actToVal - minValue)) /
|
||||||
|
(maxValue - minValue)
|
||||||
|
+ START_ANGLE_DEG.toFloat()).toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
val middleX = getMiddleX()
|
||||||
|
val middleY = getMiddleY()
|
||||||
|
val radius = getRadius()
|
||||||
|
|
||||||
|
if (middle < 360) {
|
||||||
|
arc(
|
||||||
|
middleX,
|
||||||
|
middleY,
|
||||||
|
radius,
|
||||||
|
START_ANGLE_DEG,
|
||||||
|
middle,
|
||||||
|
KnobVolumeCls.name
|
||||||
|
)
|
||||||
|
arc(
|
||||||
|
middleX,
|
||||||
|
middleY,
|
||||||
|
radius,
|
||||||
|
middle,
|
||||||
|
360,
|
||||||
|
KnobVolumeBackgroundCls.name
|
||||||
|
)
|
||||||
|
arc(
|
||||||
|
middleX,
|
||||||
|
middleY,
|
||||||
|
radius,
|
||||||
|
0,
|
||||||
|
END_ANGLE_DEG,
|
||||||
|
KnobVolumeBackgroundCls.name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arc(
|
||||||
|
middleX,
|
||||||
|
middleY,
|
||||||
|
radius,
|
||||||
|
START_ANGLE_DEG,
|
||||||
|
middle,
|
||||||
|
KnobVolumeCls.name
|
||||||
|
)
|
||||||
|
arc(
|
||||||
|
middleX,
|
||||||
|
middleY,
|
||||||
|
radius,
|
||||||
|
middle,
|
||||||
|
END_ANGLE_DEG,
|
||||||
|
KnobVolumeBackgroundCls.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
span(KnobTextCls.name) {
|
||||||
|
+label
|
||||||
|
}
|
||||||
|
|
||||||
|
val renderedValue = renderer(actualToValue(actualValue))
|
||||||
|
span(KnobValueCls.name) {
|
||||||
|
+renderedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelFunction = ::wheelFunction
|
||||||
|
|
||||||
|
/* onMouseDownFunction = {
|
||||||
|
if (it is MouseEvent) {
|
||||||
|
activated = true
|
||||||
|
mouseX = it.clientX.toDouble()
|
||||||
|
mouseY = it.clientY.toDouble()
|
||||||
|
startValue = actualValue
|
||||||
|
|
||||||
|
mainView.globalMouseListener = { me ->
|
||||||
|
if (activated && me.buttons == 1.toShort()) {
|
||||||
|
setValueByMouseDelta(me, actualMinimumValue, actualMaximumValue, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUpFunction = {
|
||||||
|
if (it is MouseEvent) {
|
||||||
|
activated = false
|
||||||
|
mainView.globalMouseListener = null
|
||||||
|
requestUpdate()
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wheelFunction(event: Event) {
|
||||||
|
println("onMouseWheelFunction: $event")
|
||||||
|
if (event is WheelEvent) {
|
||||||
|
val delta = if (event.deltaY > 0) {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
-1.0
|
||||||
|
} //it.deltaY / 250.0
|
||||||
|
|
||||||
|
var newValue = actualValue - delta * step
|
||||||
|
|
||||||
|
newValue = min(newValue, actualMaximumValue)
|
||||||
|
newValue = max(newValue, actualMinimumValue)
|
||||||
|
|
||||||
|
actualValue = newValue
|
||||||
|
|
||||||
|
callback(actualToValue(newValue))
|
||||||
|
|
||||||
|
requestUpdate()
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setValueByMouseDelta(
|
||||||
|
it: MouseEvent,
|
||||||
|
minValue: Double = 0.0,
|
||||||
|
maxValue: Double = 5.0,
|
||||||
|
callback: (value: Double) -> Unit
|
||||||
|
) {
|
||||||
|
val deltaX = it.clientX.toDouble() - mouseX
|
||||||
|
val deltaY = it.clientY.toDouble() - mouseY
|
||||||
|
var length = -deltaX + deltaY
|
||||||
|
|
||||||
|
if (it.offsetX < mouseX || it.offsetY < mouseY) {
|
||||||
|
length = -length
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = startValue + length * pixelStep
|
||||||
|
|
||||||
|
if (discrete) {
|
||||||
|
value -= (value % step)
|
||||||
|
}
|
||||||
|
|
||||||
|
value = max(value, minValue)
|
||||||
|
value = min(value, maxValue)
|
||||||
|
|
||||||
|
actualValue = value
|
||||||
|
callback(actualToValue(value))
|
||||||
|
|
||||||
|
requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : CssId("knob") {
|
||||||
|
object KnobCls : CssName
|
||||||
|
object KnobSvgCls : CssName
|
||||||
|
object KnobTextCls : CssName
|
||||||
|
object KnobValueCls : CssName
|
||||||
|
object KnobBackgroundCls : CssName
|
||||||
|
|
||||||
|
object KnobVolumeCls : CssName
|
||||||
|
object KnobVolumeBackgroundCls : CssName
|
||||||
|
|
||||||
|
init {
|
||||||
|
defineCss {
|
||||||
|
select(cls(KnobCls)) {
|
||||||
|
position(Position.relative)
|
||||||
|
margin(5.px)
|
||||||
|
|
||||||
|
and(cls(ActiveCls)) {}
|
||||||
|
|
||||||
|
select(cls(KnobSvgCls)) {
|
||||||
|
plain("stroke", "none")
|
||||||
|
plain("stroke-opacity", "1.0")
|
||||||
|
plain("fill", "none")
|
||||||
|
plain("fill-opacity", "0.0")
|
||||||
|
position(Position.absolute)
|
||||||
|
backgroundColor(Color.transparent)
|
||||||
|
|
||||||
|
and(cls(ActiveCls)) {
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
borderRadius(4.px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select(cls(KnobTextCls)) {
|
||||||
|
position(Position.absolute)
|
||||||
|
width(100.prc)
|
||||||
|
textAlign(TextAlign.center)
|
||||||
|
fontSize(1.0.em)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
select(cls(KnobValueCls)) {
|
||||||
|
position(Position.absolute)
|
||||||
|
width(100.prc)
|
||||||
|
top((48).prc)
|
||||||
|
textAlign(TextAlign.center)
|
||||||
|
fontSize(1.1.em)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select(cls(KnobVolumeCls)) {
|
||||||
|
plain("fill", Color.transparent)
|
||||||
|
plain("stroke", Css.currentStyle.mainFontColor)
|
||||||
|
color(Css.currentStyle.mainFontColor)
|
||||||
|
plain("stroke-width", "8")
|
||||||
|
//plain("stroke-dasharray", "4")
|
||||||
|
plain("fill-opacity", "0.5")
|
||||||
|
}
|
||||||
|
|
||||||
|
select(cls(KnobVolumeBackgroundCls)) {
|
||||||
|
plain("fill", Color.transparent)
|
||||||
|
plain("stroke", Css.currentStyle.mainFontColor.darken(40))
|
||||||
|
color(Css.currentStyle.mainFontColor.darken(20))
|
||||||
|
plain("stroke-width", "5")
|
||||||
|
//plain("stroke-dasharray", "4")
|
||||||
|
plain("fill-opacity", "0.5")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package nl.astraeus.vst.ui.components
|
||||||
|
|
||||||
|
import nl.astraeus.vst.util.formatDouble
|
||||||
|
import kotlin.math.log10
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User: rnentjes
|
||||||
|
* Date: 26-11-17
|
||||||
|
* Time: 16:52
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ExpKnobComponent(
|
||||||
|
value: Double = 1.0,
|
||||||
|
label: String = "",
|
||||||
|
minValue: Double = 0.0,
|
||||||
|
maxValue: Double = 5.0,
|
||||||
|
step: Double = 0.1,
|
||||||
|
width: Int = 50,
|
||||||
|
height: Int = 60,
|
||||||
|
renderer: (Double) -> String = { nv -> formatDouble(nv, 3) },
|
||||||
|
callback: (Double) -> Unit = {}
|
||||||
|
) : BaseKnobComponent(
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
minValue,
|
||||||
|
maxValue,
|
||||||
|
log10(maxValue / (maxValue - step)),
|
||||||
|
false,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
0.005,
|
||||||
|
{ log10(it) },
|
||||||
|
{ 10.0.pow(it) },
|
||||||
|
renderer,
|
||||||
|
callback
|
||||||
|
)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package nl.astraeus.vst.ui.components
|
||||||
|
|
||||||
|
import nl.astraeus.vst.util.formatDouble
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User: rnentjes
|
||||||
|
* Date: 26-11-17
|
||||||
|
* Time: 16:52
|
||||||
|
*/
|
||||||
|
|
||||||
|
class KnobComponent(
|
||||||
|
value: Double = 1.0,
|
||||||
|
label: String = "",
|
||||||
|
minValue: Double = 0.0,
|
||||||
|
maxValue: Double = 5.0,
|
||||||
|
step: Double = 0.1,
|
||||||
|
pixelStep: Double = step / 25.0,
|
||||||
|
width: Int = 50,
|
||||||
|
height: Int = 60,
|
||||||
|
renderer: (Double) -> String = { nv -> formatDouble(nv, 2) },
|
||||||
|
callback: (Double) -> Unit = {}
|
||||||
|
) : BaseKnobComponent(
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
minValue,
|
||||||
|
maxValue,
|
||||||
|
step,
|
||||||
|
true,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pixelStep,
|
||||||
|
{ it },
|
||||||
|
{ it },
|
||||||
|
renderer,
|
||||||
|
callback
|
||||||
|
)
|
||||||
123
src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt
Normal file
123
src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package nl.astraeus.vst.ui.css
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import nl.astraeus.css.properties.*
|
||||||
|
import nl.astraeus.css.style
|
||||||
|
import nl.astraeus.css.style.ConditionalStyle
|
||||||
|
import nl.astraeus.css.style.DescriptionProvider
|
||||||
|
import nl.astraeus.css.style.Style
|
||||||
|
|
||||||
|
class StyleDefinition(
|
||||||
|
val mainFontColor: Color = hsla(178, 70, 55, 1.0),
|
||||||
|
val mainBackgroundColor: Color = hsl(239, 50, 10),
|
||||||
|
//val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55),
|
||||||
|
val inputBackgroundColor: Color = mainBackgroundColor.lighten(15),
|
||||||
|
val buttonBackgroundColor: Color = mainBackgroundColor.lighten(15),
|
||||||
|
val buttonBorderColor: Color = mainFontColor.changeAlpha(0.25),
|
||||||
|
val buttonBorderWidth: Measurement = 1.px,
|
||||||
|
)
|
||||||
|
|
||||||
|
object NoTextSelectCls : CssName {
|
||||||
|
override val name = "no-text-select"
|
||||||
|
}
|
||||||
|
|
||||||
|
object SelectedCls : CssName {
|
||||||
|
override val name = "selected"
|
||||||
|
}
|
||||||
|
|
||||||
|
object ActiveCls : CssName {
|
||||||
|
override val name = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) {
|
||||||
|
this.lighten(15)
|
||||||
|
} else {
|
||||||
|
this.darken(15)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Css {
|
||||||
|
var dynamicStyles = mutableMapOf<DescriptionProvider, ConditionalStyle.() -> Unit>()
|
||||||
|
|
||||||
|
fun DescriptionProvider.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) {
|
||||||
|
check(!dynamicStyles.containsKey(this)) {
|
||||||
|
"CssId with name ${this.description()} already defined!"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCss(conditionalStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DescriptionProvider.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) {
|
||||||
|
val elementId = this.description()
|
||||||
|
var dynamicStyleElement = document.getElementById(elementId)
|
||||||
|
|
||||||
|
dynamicStyles[this] = conditionalStyle
|
||||||
|
|
||||||
|
if (dynamicStyleElement == null) {
|
||||||
|
dynamicStyleElement = document.createElement("style")
|
||||||
|
dynamicStyleElement.id = elementId
|
||||||
|
|
||||||
|
document.head?.append(dynamicStyleElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
val css = style(conditionalStyle)
|
||||||
|
|
||||||
|
dynamicStyleElement.innerHTML = css.generateCss(minified = CssSettings.minified)
|
||||||
|
}
|
||||||
|
|
||||||
|
var darkStyle = StyleDefinition(
|
||||||
|
)
|
||||||
|
|
||||||
|
var lightStyle = StyleDefinition(
|
||||||
|
mainBackgroundColor = hsl(239 + 180, 50, 15),
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentStyle: StyleDefinition = darkStyle
|
||||||
|
|
||||||
|
fun updateStyle() {
|
||||||
|
for ((cssId, dynStyle) in dynamicStyles) {
|
||||||
|
cssId.apply {
|
||||||
|
updateCss(dynStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchLayout() {
|
||||||
|
currentStyle = if (currentStyle == darkStyle) {
|
||||||
|
lightStyle
|
||||||
|
} else {
|
||||||
|
darkStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Style.transition() {
|
||||||
|
transition("all 0.5s ease")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Style.noTextSelect() {
|
||||||
|
plain("-webkit-touch-callout", "none")
|
||||||
|
plain("-webkit-user-select", "none")
|
||||||
|
plain("-moz-user-select", "none")
|
||||||
|
plain("-ms-user-select", "none")
|
||||||
|
|
||||||
|
userSelect(UserSelect.none)
|
||||||
|
|
||||||
|
select("::selection") {
|
||||||
|
background("none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object GenericCss : CssId("generic") {
|
||||||
|
init {
|
||||||
|
fun generateStyle(): String {
|
||||||
|
val css = style {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return css.generateCss(minified = CssSettings.minified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
79
src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt
Normal file
79
src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package nl.astraeus.vst.ui.css
|
||||||
|
|
||||||
|
import nl.astraeus.css.style.DescriptionProvider
|
||||||
|
import nl.astraeus.css.style.cls
|
||||||
|
|
||||||
|
private val CAPITAL_LETTER = Regex("[A-Z]")
|
||||||
|
|
||||||
|
fun String.hyphenize(): String {
|
||||||
|
var result = replace(CAPITAL_LETTER) {
|
||||||
|
"-${it.value.lowercase()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.startsWith('-')) {
|
||||||
|
result = result.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private var nextCssId = 1
|
||||||
|
|
||||||
|
object CssSettings {
|
||||||
|
var preFix = "css"
|
||||||
|
var shortId = false
|
||||||
|
var minified = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextShortId(): String {
|
||||||
|
var id = nextCssId++
|
||||||
|
val result = StringBuilder()
|
||||||
|
|
||||||
|
while (id > 0) {
|
||||||
|
val ch = ((id % 26) + 'a'.code).toChar()
|
||||||
|
result.append(ch)
|
||||||
|
|
||||||
|
id /= 26
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CssName : DescriptionProvider {
|
||||||
|
val name: String
|
||||||
|
get() = if (CssSettings.shortId) {
|
||||||
|
nextShortId()
|
||||||
|
} else {
|
||||||
|
"${CssSettings.preFix}-${this::class.simpleName?.hyphenize() ?: this::class}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cls(): DescriptionProvider = cls(this)
|
||||||
|
|
||||||
|
fun cssName(): String = "${this::class.simpleName?.hyphenize() ?: this::class}"
|
||||||
|
|
||||||
|
override fun description() = name
|
||||||
|
}
|
||||||
|
|
||||||
|
open class CssId(name: String) : DescriptionProvider {
|
||||||
|
val name: String = if (CssSettings.shortId) {
|
||||||
|
nextShortId()
|
||||||
|
} else {
|
||||||
|
"daw-$name-css"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun description() = name
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is CssId) return false
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return name.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package nl.astraeus.vst.ui.util
|
||||||
|
|
||||||
|
import org.khronos.webgl.Uint8Array
|
||||||
|
|
||||||
|
/*
|
||||||
|
fun uInt8ArrayOf(vararg values: Int): Uint8Array {
|
||||||
|
return Uint8Array(values.map { it.toByte() }.toTypedArray())
|
||||||
|
}
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user