Move stuff in base

This commit is contained in:
2024-08-09 19:54:36 +02:00
parent 8d529b0a45
commit 40ba59e7bd
27 changed files with 1595 additions and 36 deletions

View File

@@ -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")
}
}
}

View File

@@ -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"

View 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()
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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()) {
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package nl.astraeus.vst.util
package nl.astraeus.vst.ui.util
import kotlinx.html.SVG
import kotlinx.html.unsafe

View 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")
}
}
}
}
}
}
}

View 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)
}
}

View 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
}
}

View 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()
}
}
}
}

View 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
}
}

View 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()
}
}
}
}

View 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()
}

View 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

View File

@@ -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)
}
}

View 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()
}

View 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()
}

View 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)
}
}

View File

@@ -0,0 +1,5 @@
package nl.astraeus.vst.base.web
class VstSession(
val patchId: String
)

View File

@@ -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()
}
}

View File

@@ -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")
}
}
}
}
}

View File

@@ -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
)

View File

@@ -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
)

View 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)
}
}
}
}

View 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()
}
}

View File

@@ -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())
}
*/