From 40ba59e7bd0bd0381e6f6d3feb41f35f946519d6 Mon Sep 17 00:00:00 2001 From: rnentjes Date: Fri, 9 Aug 2024 19:54:36 +0200 Subject: [PATCH] Move stuff in base --- build.gradle.kts | 30 +- kotlin-js-store/yarn.lock | 5 + .../kotlin/nl/astraeus/vst/logger/Logger.kt | 53 +++ .../vst/ui/components/BaseKnobComponent.kt | 70 ++-- .../vst/ui/components/ExpKnobComponent.kt | 2 +- .../ui/components/KeyboardInputComponent.kt | 29 ++ .../nl/astraeus/vst/ui}/util/SVGFunctions.kt | 2 +- .../nl/astraeus/vst/ui/view/BaseVstView.kt | 98 ++++++ .../kotlin/nl/astraeus/vst/base/Settings.kt | 44 +++ .../kotlin/nl/astraeus/vst/base/db/BaseDao.kt | 170 ++++++++++ .../nl/astraeus/vst/base/db/Database.kt | 84 +++++ .../kotlin/nl/astraeus/vst/base/db/Entity.kt | 16 + .../nl/astraeus/vst/base/db/Migrations.kt | 97 ++++++ .../nl/astraeus/vst/base/db/PatchDao.kt | 31 ++ .../nl/astraeus/vst/base/db/PatchEntity.kt | 12 + .../vst/base/db/PatchEntityQueryProvider.kt | 64 ++++ .../nl/astraeus/vst/base/web/GenerateId.kt | 16 + .../kotlin/nl/astraeus/vst/base/web/Index.kt | 46 +++ .../astraeus/vst/base/web/RequestHandler.kt | 134 ++++++++ .../nl/astraeus/vst/base/web/Session.kt | 5 + .../astraeus/vst/base/web/UndertowServer.kt | 35 ++ .../vst/ui/components/BaseKnobComponent.kt | 304 ++++++++++++++++++ .../vst/ui/components/ExpKnobComponent.kt | 37 +++ .../vst/ui/components/KnobComponent.kt | 36 +++ .../kotlin/nl/astraeus/vst/ui/css/Css.kt | 123 +++++++ .../kotlin/nl/astraeus/vst/ui/css/CssName.kt | 79 +++++ .../nl/astraeus/vst/ui/util/MidiUtil.kt | 9 + 27 files changed, 1595 insertions(+), 36 deletions(-) create mode 100644 src/commonMain/kotlin/nl/astraeus/vst/logger/Logger.kt create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardInputComponent.kt rename src/{commonMain/kotlin/nl/astraeus/vst => jsMain/kotlin/nl/astraeus/vst/ui}/util/SVGFunctions.kt (99%) create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/ui/view/BaseVstView.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/Settings.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntityQueryProvider.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/web/GenerateId.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/web/Index.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/web/RequestHandler.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/web/Session.kt create mode 100644 src/jvmMain/kotlin/nl/astraeus/vst/base/web/UndertowServer.kt create mode 100644 src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt create mode 100644 src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt create mode 100644 src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/KnobComponent.kt create mode 100644 src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt create mode 100644 src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt create mode 100644 src/wasmJsMain/kotlin/nl/astraeus/vst/ui/util/MidiUtil.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8ae307a..e46493a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,13 @@ plugins { - kotlin("multiplatform") version "2.0.0" + kotlin("multiplatform") version "2.0.20-Beta2" id("maven-publish") } group = "nl.astraeus" -version = "1.0.0-SNAPSHOT" +version = "1.1.0-SNAPSHOT" repositories { + mavenLocal() mavenCentral() } @@ -19,21 +20,40 @@ kotlin { browser { } } + jvm { + withJava() + } sourceSets { val commonMain by getting { 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 { dependencies { - implementation("nl.astraeus:kotlin-komponent-js:1.2.2") + implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT") } } val jsTest by getting { 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") } } } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 1e9f038..d6289ee 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -52,6 +52,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@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": version "3.1.2" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" diff --git a/src/commonMain/kotlin/nl/astraeus/vst/logger/Logger.kt b/src/commonMain/kotlin/nl/astraeus/vst/logger/Logger.kt new file mode 100644 index 0000000..e60eadd --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/vst/logger/Logger.kt @@ -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() + } +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt index e6918c7..e60f82a 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt @@ -1,11 +1,17 @@ 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.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.komp.HtmlBuilder 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.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 nl.astraeus.vst.ui.util.arc +import nl.astraeus.vst.ui.util.height +import nl.astraeus.vst.ui.util.width import org.w3c.dom.events.MouseEvent import org.w3c.dom.events.WheelEvent import kotlin.math.PI @@ -77,10 +83,12 @@ open class BaseKnobComponent( width(width) height(height) + val actToVal = actualToValue(actualValue) + console.log("actualValue", actualValue, "actToVal", actToVal) val middle = ( ((ANGLE_RANGE_DEG.toFloat() * - (actualValue - actualMinimumValue)) / - (actualMaximumValue - actualMinimumValue) + (actToVal - minValue)) / + (maxValue - minValue) + START_ANGLE_DEG.toFloat()).toInt() ) @@ -143,28 +151,7 @@ open class BaseKnobComponent( +renderedValue } - onMouseWheelFunction = { - 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() - } - } + onWheelFunction = ::wheelFunction /* onMouseDownFunction = { 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( it: MouseEvent, minValue: Double = 0.0, diff --git a/src/jsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt index cb05634..bdd2c9b 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt @@ -18,7 +18,7 @@ class ExpKnobComponent( step: Double = 0.1, width: Int = 50, height: Int = 60, - renderer: (Double) -> String = { nv -> formatDouble(nv, 2) }, + renderer: (Double) -> String = { nv -> formatDouble(nv, 3) }, callback: (Double) -> Unit = {} ) : BaseKnobComponent( value, diff --git a/src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardInputComponent.kt b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardInputComponent.kt new file mode 100644 index 0000000..6bccbed --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardInputComponent.kt @@ -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()) { + + } + } + } + + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/astraeus/vst/util/SVGFunctions.kt b/src/jsMain/kotlin/nl/astraeus/vst/ui/util/SVGFunctions.kt similarity index 99% rename from src/commonMain/kotlin/nl/astraeus/vst/util/SVGFunctions.kt rename to src/jsMain/kotlin/nl/astraeus/vst/ui/util/SVGFunctions.kt index 4fd946c..ae450e9 100644 --- a/src/commonMain/kotlin/nl/astraeus/vst/util/SVGFunctions.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/ui/util/SVGFunctions.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.util +package nl.astraeus.vst.ui.util import kotlinx.html.SVG import kotlinx.html.unsafe diff --git a/src/jsMain/kotlin/nl/astraeus/vst/ui/view/BaseVstView.kt b/src/jsMain/kotlin/nl/astraeus/vst/ui/view/BaseVstView.kt new file mode 100644 index 0000000..59cd7ef --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/ui/view/BaseVstView.kt @@ -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") + } + } + } + } + } + } + +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/Settings.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/Settings.kt new file mode 100644 index 0000000..ecf1d83 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/Settings.kt @@ -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) { + 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) + } +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt new file mode 100644 index 0000000..6a4dbe7 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt @@ -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( + val sql: String, + val prepareParameters: T.(PreparedStatement) -> Unit +) + +data class SqlQuery( + val sql: String, + val resultMapper: (ResultSet) -> T +) + +abstract class QueryProvider { + abstract val tableName: String + open val idQuery: String + get() = "SELECT * FROM $tableName WHERE ID = ?" + abstract val resultSetMapper: (ResultSet) -> T + open val find: SqlQuery + get() = SqlQuery( + idQuery, + resultSetMapper + ) + abstract val insert: SqlStatement + abstract val update: SqlStatement + open val delete: SqlStatement + get() = SqlStatement( + "DELETE FROM $tableName WHERE ID = ?" + ) { ps -> + ps.setLong(1, getPK()[0] as Long) + } +} + +abstract class BaseDao { + abstract val queryProvider: QueryProvider + 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, + prepareParameters: (PreparedStatement) -> Unit, + ): List { + return Database.transaction { con -> + log.debug { "Executing query [$label] - [${statement.sql}]" } + val result = mutableListOf() + + 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, + 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, + 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 + } + +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt new file mode 100644 index 0000000..3540b65 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt @@ -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.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() + + 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 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() + } + } + } + +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt new file mode 100644 index 0000000..c11f9f3 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt @@ -0,0 +1,16 @@ +package nl.astraeus.vst.base.db + +interface Entity { + fun getPK(): Array + fun setPK(pks: Array) +} + +interface EntityId : Entity { + var id: Long + + override fun getPK(): Array = arrayOf(id) + + override fun setPK(pks: Array) { + id = pks[0] as Long + } +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt new file mode 100644 index 0000000..b59c239 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt @@ -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 + ) { + 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.. +/* 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() + } + } + } + +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt new file mode 100644 index 0000000..6b27106 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt @@ -0,0 +1,31 @@ +package nl.astraeus.vst.base.db + +object PatchDao : BaseDao() { + + override val queryProvider: QueryProvider + 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() + +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt new file mode 100644 index 0000000..8d35cad --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt @@ -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 \ No newline at end of file diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntityQueryProvider.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntityQueryProvider.kt new file mode 100644 index 0000000..8de94b3 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntityQueryProvider.kt @@ -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() { + 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 + 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 + 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) + } +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/web/GenerateId.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/GenerateId.kt new file mode 100644 index 0000000..61d6960 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/GenerateId.kt @@ -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() +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/web/Index.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/Index.kt new file mode 100644 index 0000000..72effd3 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/Index.kt @@ -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() +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/web/RequestHandler.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/RequestHandler.kt new file mode 100644 index 0000000..746c9af --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/RequestHandler.kt @@ -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) + } +} diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/web/Session.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/Session.kt new file mode 100644 index 0000000..57a7889 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/Session.kt @@ -0,0 +1,5 @@ +package nl.astraeus.vst.base.web + +class VstSession( + val patchId: String +) \ No newline at end of file diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/base/web/UndertowServer.kt b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/UndertowServer.kt new file mode 100644 index 0000000..2956044 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/vst/base/web/UndertowServer.kt @@ -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() + } + +} \ No newline at end of file diff --git a/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt new file mode 100644 index 0000000..15af293 --- /dev/null +++ b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/BaseKnobComponent.kt @@ -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") + } + } + } + } + +} diff --git a/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt new file mode 100644 index 0000000..bdd2c9b --- /dev/null +++ b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/ExpKnobComponent.kt @@ -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 +) diff --git a/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/KnobComponent.kt b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/KnobComponent.kt new file mode 100644 index 0000000..a5dd13e --- /dev/null +++ b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/components/KnobComponent.kt @@ -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 +) diff --git a/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt new file mode 100644 index 0000000..1d2e8d7 --- /dev/null +++ b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt @@ -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 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) + } + } + } + +} diff --git a/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt new file mode 100644 index 0000000..c077a29 --- /dev/null +++ b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt @@ -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() + } + +} diff --git a/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/util/MidiUtil.kt b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/util/MidiUtil.kt new file mode 100644 index 0000000..294a6b6 --- /dev/null +++ b/src/wasmJsMain/kotlin/nl/astraeus/vst/ui/util/MidiUtil.kt @@ -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()) +} +*/