Compare commits

...

9 Commits

Author SHA1 Message Date
ed0b76dc06 Update to version 2.2.0-alpha-5 and adjust octave display logic in KeyboardComponent 2025-06-15 13:16:23 +02:00
7c87274a04 Update to version 2.2.0-alpha-4 and bump kotlin-komponent to 1.2.8 2025-06-12 19:48:27 +02:00
5da8424c40 Remove KeyboardInputComponent and add file upload and WebSocket support
Deleted unused `KeyboardInputComponent`. Added file upload functionality to `MainView` with WebSocket event handling. Introduced `WebsocketClient` class for managing server connection and file transmission. Enhanced `KeyboardComponent` with configurable key rounding. Updated `WebsocketHandler` for improved binary message handling and storage.
2025-06-10 21:08:00 +02:00
8ee8f17f96 Refactor WebsocketHandler into a standalone class file 2025-06-10 20:08:28 +02:00
0bdaa5c94f Add binary file handling with hash-based storage and retrieval
Introduced hash-based storage for binary files in `RequestHandler`, with subdirectory organization. Added methods for creating hashes, saving, and retrieving files. Enabled binary file transmission via WebSocket commands. Updated `Settings` to support configurable data directory.
2025-06-10 20:07:11 +02:00
3746ced387 Refactor SampleEntity to BinaryEntity and update related files and queries 2025-06-10 19:46:19 +02:00
4d7c46093c Update package declarations for SampleEntity files
Standardized package paths in `SampleEntity`, `SampleEntityQueryProvider`, and `SampleDao` from `nl.astraeus.vst.chip.db` to `nl.astraeus.vst.base.db`.
2025-06-10 19:35:10 +02:00
2871697329 Update to version 2.2.0-alpha-2 and add database query provider
Bumped project version to `2.2.0-alpha-2` in `build.gradle.kts`. Introduced `SampleEntity`, `SamplePartEntity`, and `SampleEntityQueryProvider` for database handling. Added `SampleDao` with a sample query function. Updated SVG utilities with a `viewbox` extension and enhanced `RequestHandler` to set content type for HTML responses.
2025-06-09 14:06:17 +02:00
c1f756eb79 Update to version 2.1.2 in build.gradle.kts 2025-06-07 13:21:18 +02:00
13 changed files with 670 additions and 290 deletions

View File

@@ -10,7 +10,7 @@ plugins {
}
group = "nl.astraeus"
version = "2.1.1-SNAPSHOT"
version = "2.2.0-alpha-5"
repositories {
mavenCentral()
@@ -47,7 +47,7 @@ kotlin {
}
val jsMain by getting {
dependencies {
api("nl.astraeus:kotlin-komponent:1.2.7")
api("nl.astraeus:kotlin-komponent:1.2.8")
}
}
val jsTest by getting {

View File

@@ -38,6 +38,7 @@ class KeyboardComponent(
initialOctave: Int = 4,
val keyboardWidth: Int = 210,
val keyboardHeight: Int = keyboardWidth / 2,
val rounding: Int = 4,
val onNoteDown: (Int) -> Unit = {},
val onNoteUp: (Int) -> Unit = {}
) : Komponent() {
@@ -163,7 +164,8 @@ class KeyboardComponent(
div(KeyboardOctaveCls.name) {
// Show current octave the piano is being played at
+"Octave: $octave"
// minus one to keep it in line with custom notes ranges
+"Octave: ${octave -1}"
}
}
@@ -222,7 +224,7 @@ class KeyboardComponent(
0,
whiteKeyWidth,
keyboardHeight,
0,
rounding,
if (isPressed) WhiteKeyPressedCls.name else WhiteKeyCls.name
)
}
@@ -238,7 +240,7 @@ class KeyboardComponent(
0,
blackKeyWidth,
blackKeyHeight,
0,
rounding,
if (isPressed) BlackKeyPressedCls.name else BlackKeyCls.name
)
}

View File

@@ -1,29 +0,0 @@
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

@@ -14,6 +14,10 @@ fun SVG.height(height: Int) {
this.attributes["height"] = "$height"
}
fun SVG.viewbox(viewbox: String) {
this.attributes["viewbox"] = viewbox
}
fun SVG.svgStyle(
name: String,
vararg props: Pair<String, String>
@@ -41,12 +45,12 @@ fun SVG.rect(
y: Int,
width: Int,
height: Int,
rx: Int,
cls: String
rounding: Int,
cls: String,
) {
this.unsafe {
+ """
<rect class="$cls" x="$x" y="$y" width="$width" height="$height" rx="$rx" />
<rect class="$cls" x="$x" y="$y" width="$width" height="$height" rx="$rounding" rx="$rounding" />
""".trimIndent()
}
}

View File

@@ -8,10 +8,11 @@ object Settings {
var port = 9004
var connectionTimeout = 30000
var jdbcStatsPort = 6001
var dataDir = "data"
var jdbcDriver = "nl.astraeus.jdbc.Driver"
val jdbcConnectionUrl
get() = "jdbc:stat:webServerPort=$jdbcStatsPort:jdbc:sqlite:data/vst.db"
get() = "jdbc:stat:webServerPort=$jdbcStatsPort:jdbc:sqlite:$dataDir/vst.db"
var jdbcUser = "sa"
var jdbcPassword = ""
@@ -35,6 +36,8 @@ object Settings {
port = properties.getProperty("port", port.toString()).toInt()
jdbcStatsPort = properties.getProperty("jdbcStatsPort", jdbcStatsPort.toString()).toInt()
dataDir = properties.getProperty("dataDir", dataDir)
connectionTimeout =
properties.getProperty("connectionTimeout", connectionTimeout.toString()).toInt()
jdbcDriver = properties.getProperty("jdbcDriver", jdbcDriver)

View File

@@ -0,0 +1,11 @@
package nl.astraeus.vst.base.db
object BinaryDao {
val queryProvider = SampleEntityQueryProvider
fun getSample(waveHash: String): ByteArray {
return byteArrayOf()
}
}

View File

@@ -0,0 +1,49 @@
package nl.astraeus.vst.base.db
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
data class SampleEntity(
var sha1Hash: String,
var filename: String,
var length: Int,
var created: Instant = Clock.System.now(),
var updated: Instant = Clock.System.now(),
) : Entity {
override fun getPK(): Array<Any> = arrayOf(sha1Hash)
override fun setPK(pks: Array<Any>) {
sha1Hash = pks[0] as String
}
}
data class SamplePartEntity(
var sha1Hash: String,
var part: Int,
var from: Int,
var to: Int,
var data: ByteArray,
) : Entity {
override fun getPK(): Array<Any> = arrayOf(sha1Hash, part)
override fun setPK(pks: Array<Any>) {
sha1Hash = pks[0] as String
part = pks[1] as Int
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SamplePartEntity) return false
if (sha1Hash != other.sha1Hash) return false
if (part != other.part) return false
return true
}
override fun hashCode(): Int {
var result = sha1Hash.hashCode()
result = 31 * result + part
return result
}
}

View File

@@ -0,0 +1,45 @@
package nl.astraeus.vst.base.db
import java.sql.ResultSet
val BINARY_CREATE_QUERY = """
CREATE TABLE BINARIESYSAMPLES (
SHA1HASH TEXT,
FILENAME TEXT,
LENGTH NUMBER,
CREATED TIMESTAMP
)
""".trimIndent()
object SampleEntityQueryProvider : QueryProvider<SampleEntity>() {
override val tableName: String
get() = "SAMPLES"
override val resultSetMapper: (ResultSet) -> SampleEntity
get() = { rs ->
SampleEntity(
rs.getString(1),
rs.getString(2),
rs.getInt(3),
rs.getTimestamp(4).toDateTimeInstant()
)
}
override val insert: SqlStatement<SampleEntity>
get() = SqlStatement(
"""
INSERT INTO $tableName (
SHA1HASH,
LENGTH,
CREATED
) VALUES (
?,?,?,?
)
""".trimIndent()
) { ps ->
ps.setString(1, sha1Hash)
ps.setString(2, filename)
ps.setInt(3, length)
ps.setTimestamp(4, updated.toSqlTimestamp())
}
override val update: SqlStatement<SampleEntity>
get() = TODO("Not yet implemented")
}

View File

@@ -10,16 +10,6 @@ import io.undertow.server.session.Session
import io.undertow.server.session.SessionCookieConfig
import io.undertow.server.session.SessionManager
import io.undertow.util.Headers
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
object VstSessionConfig {
@@ -30,61 +20,6 @@ object VstSessionConfig {
}
}
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) {
@@ -112,6 +47,7 @@ class PatchHandler(
}
httpSession?.setAttribute("html-session", VstSession(patchId))
exchange.responseHeaders.put(Headers.CONTENT_TYPE, "text/html; charset=utf-8")
exchange.responseSender.send(generateIndex(title, scriptName, null))
} else {
val patchId = generateId()

View File

@@ -0,0 +1,197 @@
package nl.astraeus.vst.base.web
import io.undertow.server.session.Session
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.Settings
import nl.astraeus.vst.base.db.Database
import nl.astraeus.vst.base.db.PatchDao
import nl.astraeus.vst.base.db.PatchEntity
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.security.MessageDigest
class WebsocketHandler(
val session: Session?
) : AbstractReceiveListener(), WebSocketConnectionCallback {
companion object {
// Ensure the data directory exists
private val filesDir = File(Settings.dataDir, "files").apply {
if (!exists()) {
mkdirs()
}
}
fun fileExists(hash: String): Boolean {
check(hash.length == 64) { "Hash must be 64 characters long" }
var currentDir = filesDir
var remaining = hash
while(remaining.length > 8) {
val subDir = remaining.substring(0, 8)
currentDir = File(currentDir, subDir)
if (!currentDir.exists()) {
return false
}
remaining = remaining.substring(8)
}
return File(currentDir, remaining).exists()
}
// Get file from hash, using subdirectories based on hash
fun getFileFromHash(hash: String): File {
check(hash.length == 64) { "Hash must be 64 characters long" }
var currentDir = filesDir
var remaining = hash
while(remaining.length > 8) {
val subDir = remaining.substring(0, 8)
currentDir = File(currentDir, subDir)
if (!currentDir.exists()) {
currentDir.mkdirs()
}
remaining = remaining.substring(8)
}
return File(currentDir, remaining)
}
// Create SHA-1 hash from binary data
fun createHashFromBytes(bytes: ByteArray): String {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(bytes)
return digest.joinToString("") { "%02x".format(it) }
}
}
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
)
}
}
}
}
"LOAD_BINARY" -> {
val hash = value.trim()
if (hash.isNotEmpty()) {
if (fileExists(hash)) {
val file = getFileFromHash(hash)
if (file.exists() && file.isFile) {
val bytes = file.readBytes()
WebSockets.sendBinary(
ByteBuffer.wrap(bytes),
channel,
null
)
}
}
}
}
}
}
}
override fun onFullBinaryMessage(
channel: WebSocketChannel?,
message: BufferedBinaryMessage?
) {
// Process binary message: create hash from binary, save file in data/files/ directory,
// sub directories are 5 characters of the hash per directory
if (channel != null && message != null) {
try {
// Get the binary data
val pooled = message.data
val resources = pooled.resource
// Collect all bytes from all buffers
var totalSize = 0
// First pass: collect all bytes and calculate total size
for (buffer in resources) {
totalSize += buffer.remaining()
}
// Combine all bytes into a single array
val combinedBytes = ByteArray(totalSize)
var position = 0
for (buffer in resources) {
val remaining = buffer.remaining()
buffer.get(combinedBytes, position, remaining)
position += remaining
}
// Create hash from combined binary data
val hash = createHashFromBytes(combinedBytes)
// Save file in data/files/ directory with subdirectories
val file = getFileFromHash(hash)
// Use FileOutputStream to write all bytes at once
FileOutputStream(file).use { outputStream ->
outputStream.write(combinedBytes)
}
// Send the hash back to the client
WebSockets.sendText("BINARY_SAVED\n$hash", channel, null)
// Free the pooled resource after processing
pooled.free()
} catch (e: Exception) {
WebSockets.sendText("ERROR\n${e.message}", channel, null)
}
}
}
}

View File

@@ -5,6 +5,7 @@ import nl.astraeus.komp.Komponent
import nl.astraeus.komp.UnsafeMode
import nl.astraeus.vst.ui.css.CssSettings
import nl.astraeus.vst.ui.view.MainView
import nl.astraeus.vst.ui.ws.WebsocketClient
val mainView: MainView by lazy {
MainView()
@@ -16,4 +17,8 @@ fun main() {
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
Komponent.create(document.body!!, mainView)
WebsocketClient.connect {
println("Connected")
}
}

View File

@@ -2,11 +2,15 @@
package nl.astraeus.vst.ui.view
import kotlinx.html.InputType
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.hr
import kotlinx.html.input
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.option
import kotlinx.html.org.w3c.dom.events.Event
import kotlinx.html.select
import kotlinx.html.span
import kotlinx.html.style
@@ -37,6 +41,11 @@ import nl.astraeus.vst.ui.css.Css.defineCss
import nl.astraeus.vst.ui.css.Css.noTextSelect
import nl.astraeus.vst.ui.css.CssName
import nl.astraeus.vst.ui.css.hover
import nl.astraeus.vst.ui.ws.WebsocketClient
import org.w3c.dom.HTMLInputElement
import org.w3c.files.File
import org.w3c.files.FileList
import org.w3c.files.get
class MainView : Komponent() {
private var messages: MutableList<String> = ArrayList()
@@ -113,6 +122,16 @@ class MainView : Komponent() {
}
*/
}
span {
+"Upload file: "
input {
type = InputType.file
onChangeFunction = {
fileInputSelectHandler(it)
requestUpdate()
}
}
}
}
}
div {
@@ -134,209 +153,226 @@ class MainView : Komponent() {
}
hr {}
div {
include(KeyboardComponent(
keyboardWidth = keyboardWidth,
keyboardHeight = keyboardWidth / 2,
onNoteDown = { println("Note down: $it") },
onNoteUp = { println("Note up: $it") },
))
include(
KeyboardComponent(
keyboardWidth = keyboardWidth,
keyboardHeight = keyboardWidth / 2,
onNoteDown = { println("Note down: $it") },
onNoteUp = { println("Note up: $it") },
)
)
}
/*
div {
span(ButtonBarCss.name) {
+"SAVE"
onClickFunction = {
val patch = VstChipWorklet.save().copy(
midiId = Midi.currentInput?.id ?: "",
midiName = Midi.currentInput?.name ?: ""
)
/*
div {
span(ButtonBarCss.name) {
+"SAVE"
onClickFunction = {
val patch = VstChipWorklet.save().copy(
midiId = Midi.currentInput?.id ?: "",
midiName = Midi.currentInput?.name ?: ""
)
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
}
}
span(ButtonBarCss.name) {
+"STOP"
onClickFunction = {
VstChipWorklet.postDirectlyToWorklet(
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
.data.buffer.data
)
}
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
}
}
span(ButtonBarCss.name) {
+"STOP"
onClickFunction = {
VstChipWorklet.postDirectlyToWorklet(
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
.data.buffer.data
)
}
}
}
*/
div(ControlsCss.name) {
include(
ExpKnobComponent(
value = 0.001,
label = "Volume",
minValue = 0.001,
maxValue = 1.0,
step = 10.0 / 127.0,
width = 100,
height = 120,
) { value ->
div(ControlsCss.name) {
include(
ExpKnobComponent(
value = 0.001,
label = "Volume",
minValue = 0.001,
maxValue = 1.0,
step = 10.0 / 127.0,
width = 100,
height = 120,
) { value ->
}
)
include(
KnobComponent(
value = 0.5,
label = "Duty cycle",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
}
)
include(
KnobComponent(
value = 0.5,
label = "Duty cycle",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
}
)
include(
KnobComponent(
value = 0.5,
label = "Duty cycle",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 500,
height = 600,
) { value ->
}
)
include(
KnobComponent(
value = 0.5,
label = "Duty cycle",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 500,
height = 600,
) { value ->
}
)
}
}
}
private fun fileInputSelectHandler(event: Event) {
val target = event.target as? HTMLInputElement
val list: FileList? = target?.files
if (list == null || list.length != 1) {
return
}
val file: File? = list[0]
file?.let { f: File ->
WebsocketClient.send(f)
}
}
companion object MainViewCss : CssName() {
object MainDivCss : CssName()
object ActiveCss : CssName()
object ButtonCss : CssName()
object ButtonBarCss : CssName()
object SelectedCss : CssName()
object NoteBarCss : CssName()
object StartSplashCss : CssName()
object StartBoxCss : CssName()
object StartButtonCss : CssName()
object ControlsCss : CssName()
init {
defineCss {
select("*") {
select("*:before") {
select("*:after") {
boxSizing(BoxSizing.borderBox)
}
)
}
}
select("html", "body") {
margin(0.px)
padding(0.px)
height(100.prc)
}
select("html", "body") {
backgroundColor(Css.currentStyle.mainBackgroundColor)
color(Css.currentStyle.mainFontColor)
fontFamily("JetbrainsMono, monospace")
fontSize(14.px)
fontWeight(FontWeight.bold)
//transition()
noTextSelect()
}
select("input", "textarea") {
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
border("none")
}
select(cls(ButtonCss)) {
margin(1.rem)
commonButton()
}
select(cls(ButtonBarCss)) {
margin(1.rem, 0.px)
commonButton()
}
select(cls(ActiveCss)) {
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
}
select(cls(NoteBarCss)) {
minHeight(4.rem)
}
select(cls(MainDivCss)) {
margin(1.rem)
}
select("select") {
plain("appearance", "none")
border("0")
outline("0")
width(20.rem)
padding(0.5.rem, 2.rem, 0.5.rem, 0.5.rem)
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
background("right 0.8em center/1.4em")
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
borderRadius(0.25.em)
}
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")
}
}
}
select(ControlsCss.cls()) {
display(Display.flex)
flexDirection(FlexDirection.row)
justifyContent(JustifyContent.flexStart)
alignItems(AlignItems.center)
margin(1.rem)
padding(1.rem)
backgroundColor(Css.currentStyle.mainBackgroundColor)
}
}
}
companion object MainViewCss : CssName() {
object MainDivCss : CssName()
object ActiveCss : CssName()
object ButtonCss : CssName()
object ButtonBarCss : CssName()
object SelectedCss : CssName()
object NoteBarCss : CssName()
object StartSplashCss : CssName()
object StartBoxCss : CssName()
object StartButtonCss : CssName()
object ControlsCss : CssName()
private fun Style.commonButton() {
display(Display.inlineBlock)
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
borderColor(Css.currentStyle.buttonBorderColor)
borderWidth(Css.currentStyle.buttonBorderWidth)
color(Css.currentStyle.mainFontColor)
init {
defineCss {
select("*") {
select("*:before") {
select("*:after") {
boxSizing(BoxSizing.borderBox)
}
}
}
select("html", "body") {
margin(0.px)
padding(0.px)
height(100.prc)
}
select("html", "body") {
backgroundColor(Css.currentStyle.mainBackgroundColor)
color(Css.currentStyle.mainFontColor)
fontFamily("JetbrainsMono, monospace")
fontSize(14.px)
fontWeight(FontWeight.bold)
//transition()
noTextSelect()
}
select("input", "textarea") {
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
border("none")
}
select(cls(ButtonCss)) {
margin(1.rem)
commonButton()
}
select(cls(ButtonBarCss)) {
margin(1.rem, 0.px)
commonButton()
}
select(cls(ActiveCss)) {
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
}
select(cls(NoteBarCss)) {
minHeight(4.rem)
}
select(cls(MainDivCss)) {
margin(1.rem)
}
select("select") {
plain("appearance", "none")
border("0")
outline("0")
width(20.rem)
padding(0.5.rem, 2.rem, 0.5.rem, 0.5.rem)
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
background("right 0.8em center/1.4em")
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
borderRadius(0.25.em)
}
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")
}
}
}
select(ControlsCss.cls()) {
display(Display.flex)
flexDirection(FlexDirection.row)
justifyContent(JustifyContent.flexStart)
alignItems(AlignItems.center)
margin(1.rem)
padding(1.rem)
backgroundColor(Css.currentStyle.mainBackgroundColor)
}
}
hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
}
private fun Style.commonButton() {
display(Display.inlineBlock)
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
borderColor(Css.currentStyle.buttonBorderColor)
borderWidth(Css.currentStyle.buttonBorderWidth)
color(Css.currentStyle.mainFontColor)
hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
}
and(SelectedCss.cls()) {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
}
and(SelectedCss.cls()) {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
}
}
}
}

View File

@@ -0,0 +1,121 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.ui.ws
import kotlinx.browser.window
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
import org.w3c.files.Blob
private const val WEBSOCKET_PART_SIZE = 65000
object WebsocketClient {
var websocket: WebSocket? = null
var interval: Int = 0
fun connect(onConnect: () -> Unit) {
close()
websocket =
if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) {
WebSocket("ws://${window.location.hostname}:${window.location.port}/ws")
} else {
WebSocket("wss://${window.location.hostname}/ws")
}
websocket?.also { ws ->
ws.onopen = {
onOpen(ws, it)
onConnect()
}
ws.onmessage = { onMessage(ws, it) }
ws.onclose = { onClose(ws, it) }
ws.onerror = { onError(ws, it) }
}
}
fun close() {
websocket?.close(-1, "Application closed socket.")
}
fun onOpen(
ws: WebSocket,
event: Event
) {
interval = window.setInterval({
val actualWs = websocket
if (actualWs == null) {
window.clearInterval(interval)
console.log("Connection to the server was lost!\\nPlease try again later.")
reconnect()
}
}, 10000)
}
fun reconnect() {
val actualWs = websocket
if (actualWs != null) {
if (actualWs.readyState == WebSocket.OPEN) {
console.log("Connection to the server was lost!\\nPlease try again later.")
} else {
window.setTimeout({
reconnect()
}, 1000)
}
} else {
connect {}
window.setTimeout({
reconnect()
}, 1000)
}
}
fun onMessage(
ws: WebSocket,
event: Event
) {
if (event is MessageEvent) {
val data = event.data
if (data is String) {
console.log("Received message: $data")
} else if (data is ArrayBuffer) {
console.log("Received binary message")
}
}
}
fun onClose(
ws: WebSocket,
event: Event
): dynamic {
websocket = null
return "dynamic"
}
fun onError(
ws: WebSocket,
event: Event
): dynamic {
console.log("Error websocket!", ws, event)
websocket = null
return "dynamic"
}
fun send(message: String) {
websocket?.send(message)
}
fun send(file: Blob) {
websocket?.send(file)
}
}