Compare commits

...

17 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
1d02a6ee16 Refactor KeyboardComponent to use immutable state for octave and pressed notes
Replaced mutable state with an immutable `KeyboardState` data class to track octave and pressed notes. Updated state management logic with functional updates for improved consistency and immutability. Simplified note handling and rendering to reference the unified state object.
2025-06-07 13:12:03 +02:00
9c9962d7db Refactor KeyboardComponent key rendering logic
Extracted white and black key rendering into separate `renderWhiteKeys` and `renderBlackKeys` functions for improved readability and modularity. Simplified drawing logic by delegating key rendering to these helper functions.
2025-06-07 11:57:17 +02:00
5c16b57ae9 Refactor KeyboardComponent with constants for note and key handling
Replaced hardcoded values for octaves, keys, and dimensions with named constants for improved readability and maintainability. Simplified calculations and loops using these constants. Enhanced clarity in key rendering and MIDI note calculations.
2025-06-07 11:39:21 +02:00
9ab909cf6c Refactor KeyboardComponent note calculation logic
Extracted MIDI note calculation into a reusable `getMidiNoteFromMousePosition` function. Replaced redundant inline logic in mouse event handlers and key drawing sections for improved readability and maintainability.
2025-06-07 11:24:30 +02:00
538aa6b9ae Update to version 2.1.1-SNAPSHOT and improve KeyboardComponent note handling
Bumped project version to 2.1.1-SNAPSHOT in `build.gradle.kts`. Enhanced `KeyboardComponent` logic to visually indicate pressed keys with new CSS classes for both white and black keys. Added `requestUpdate` calls to ensure UI updates on note changes. Simplified JVM target configuration in build script.
2025-06-06 20:21:51 +02:00
770607d5e6 Release version 2.1.0 and add MIDI broadcasting and handling
Updated `build.gradle.kts` to finalize version 2.1.0. Introduced `Broadcaster` and `Midi` classes for MIDI message broadcasting, synchronization, and handling. Added support for MIDI input/output devices with state management and message processing capabilities.
2025-06-06 19:55:44 +02:00
e6b7c9b288 Extend octave range to 0-9 and adjust MIDI note calculations 2025-06-06 19:47:05 +02:00
68a15bab8b Add octave controls to KeyboardComponent
Introduced buttons to dynamically adjust the octave within the range of 0 to 8. Added new CSS classes for layout and styling of octave controls, updated component logic with range validation, and reorganized the title and octave display for improved UI.
2025-06-06 19:32:15 +02:00
15 changed files with 1150 additions and 404 deletions

View File

@@ -10,7 +10,7 @@ plugins {
}
group = "nl.astraeus"
version = "2.1.0-SNAPSHOT"
version = "2.2.0-alpha-5"
repositories {
mavenCentral()
@@ -37,9 +37,7 @@ kotlin {
}
}
}
jvm {
withJava()
}
jvm {}
sourceSets {
val commonMain by getting {
@@ -49,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

@@ -0,0 +1,110 @@
@file:OptIn(ExperimentalJsExport::class)
package nl.astraeus.vst.midi
import kotlinx.browser.window
import org.khronos.webgl.Uint8Array
import org.w3c.dom.BroadcastChannel
import org.w3c.dom.MessageEvent
import kotlin.math.min
@JsExport
enum class MessageType {
SYNC,
MIDI
}
@JsExport
class SyncMessage(
@JsName("timeOrigin")
val timeOrigin: Double = window.performance.asDynamic().timeOrigin,
@JsName("now")
val now: Double = window.performance.now()
) {
@JsName("type")
val type: String = MessageType.SYNC.name
}
// time -> syncOrigin
// receive syn message
// syncOrigin = my timeOrigin - sync.timeOrigin
// - sync.timeOrigin = 50
// - my.timeOrigin = 100
// - syncOrigin = -50
// - sync.timeOrigin = 49
// - my.timeOrigin = 100
// - syncOrigin = update to -51
@JsExport
class MidiMessage(
@JsName("data")
val data: Uint8Array,
@JsName("timestamp")
val timestamp: dynamic = null,
@JsName("timeOrigin")
val timeOrigin: dynamic = window.performance.asDynamic().timeOrigin
) {
@JsName("type")
val type = MessageType.MIDI.name
}
object Sync {
var syncOrigin = 0.0
fun update(sync: SyncMessage) {
syncOrigin = min(syncOrigin, window.performance.asDynamic().timeOrigin - sync.timeOrigin)
}
fun now(): Double = window.performance.now() + syncOrigin
}
object Broadcaster {
val channels = mutableMapOf<Int, BroadcastChannel>()
fun getChannel(channel: Int): BroadcastChannel = channels.getOrPut(channel) {
println("Opening broadcast channel $channel")
val bcChannel = BroadcastChannel("audio-worklet-$channel")
bcChannel.onmessage = { event ->
onMessage(channel, event)
}
bcChannel
}
private fun onMessage(channel: Int, event: MessageEvent) {
val data: dynamic = event.data.asDynamic()
console.log(
"Received broadcast message on channel $channel",
event
)
if (data.type == MessageType.SYNC.name) {
val syncMessage = SyncMessage(
data.timeOrigin,
data.now
)
Sync.update(syncMessage)
} else {
console.log(
"Received broadcast message on channel $channel",
event,
window.performance,
window.performance.now() - event.timeStamp.toDouble()
)
}
}
fun send(channel: Int, message: Any) {
console.log("Sending broadcast message on channel $channel:", message)
getChannel(channel).postMessage(message)
}
fun sync() {
for (channel in channels.values) {
channel.postMessage(SyncMessage())
}
}
}

View File

@@ -0,0 +1,160 @@
package nl.astraeus.vst.midi
import kotlinx.browser.window
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.get
external class MIDIInput {
val connection: String
val id: String
val manufacturer: String
val name: String
val state: String
val type: String
val version: String
var onmidimessage: (dynamic) -> Unit
var onstatechange: (dynamic) -> Unit
fun open()
fun close()
}
external class MIDIOutput {
val connection: String
val id: String
val manufacturer: String
val name: String
val state: String
val type: String
val version: String
fun send(message: dynamic, timestamp: dynamic)
fun open()
fun close()
}
object Midi {
var outputChannel: Int = -1
var inputs = mutableListOf<MIDIInput>()
var outputs = mutableListOf<MIDIOutput>()
var currentInput: MIDIInput? = null
var currentOutput: MIDIOutput? = null
fun start(
onUpdate: () -> Unit,
) {
val navigator = window.navigator.asDynamic()
navigator.requestMIDIAccess().then(
{ midiAccess ->
val inp = midiAccess.inputs
val outp = midiAccess.outputs
console.log("Midi inputs:", inputs)
console.log("Midi outputs:", outputs)
inp.forEach() { input ->
console.log("Midi input:", input)
inputs.add(input)
console.log("Name: ${(input as? MIDIInput)?.name}")
}
outp.forEach() { output ->
console.log("Midi output:", output)
outputs.add(output)
}
onUpdate()
},
{ e ->
println("Failed to get MIDI access - $e")
}
)
}
fun setInput(
id: String,
name: String = "",
onMidiInput: (data: Uint8Array) -> Unit
) {
var selected = inputs.find { it.id == id }
if (selected == null) {
var maxMatchChar = 0
inputs.forEach {
val matchChars = matchChars(it.name, name)
if (matchChars > maxMatchChar) {
selected = it
maxMatchChar = matchChars
}
}
}
setInput(selected, onMidiInput)
}
private fun matchChars(str1: String, str2: String): Int {
var result = 0
if (str1.length > str2.length) {
for (ch in str1.toCharArray()) {
if (str2.contains(ch)) {
result++
}
}
} else {
for (ch in str2.toCharArray()) {
if (str1.contains(ch)) {
result++
}
}
}
return result
}
fun setInput(
input: MIDIInput?,
onMidiInput: (data: Uint8Array) -> Unit,
) {
console.log("Setting input", input)
currentInput?.close()
currentInput = input
currentInput?.onstatechange = { message ->
console.log("State change:", message)
}
currentInput?.onmidimessage = { message ->
val data = message.data as Uint8Array
val hex = StringBuilder()
for (index in 0 until data.length) {
hex.append(data[index].toString(16))
hex.append(" ")
}
console.log("Midi message:", hex)
onMidiInput(message.data)
}
currentInput?.open()
}
fun setOutput(
output: MIDIOutput?
) {
console.log("Setting output", output)
currentOutput?.close()
currentOutput = output
currentOutput?.open()
}
fun send(
data: Uint8Array,
timestamp: dynamic = null
) {
currentOutput?.send(data, timestamp)
}
}

View File

@@ -1,13 +1,20 @@
package nl.astraeus.vst.ui.components
import kotlinx.html.SVG
import kotlinx.html.div
import kotlinx.html.js.onMouseDownFunction
import kotlinx.html.js.onMouseLeaveFunction
import kotlinx.html.js.onMouseUpFunction
import kotlinx.html.style
import kotlinx.html.svg
import nl.astraeus.css.properties.AlignItems
import nl.astraeus.css.properties.Display
import nl.astraeus.css.properties.FlexDirection
import nl.astraeus.css.properties.FontWeight
import nl.astraeus.css.properties.JustifyContent
import nl.astraeus.css.properties.Position
import nl.astraeus.css.properties.TextAlign
import nl.astraeus.css.properties.prc
import nl.astraeus.css.properties.px
import nl.astraeus.css.properties.rem
import nl.astraeus.css.style.cls
@@ -28,24 +35,44 @@ import org.w3c.dom.events.MouseEvent
*/
class KeyboardComponent(
val title: String = "Keyboard",
val octave: Int = 4,
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() {
// Set to track which notes are currently pressed
private val pressedNotes = mutableSetOf<Int>()
// Define a data class for keyboard state
data class KeyboardState(
val octave: Int,
val pressedNotes: Set<Int> = emptySet()
)
// MIDI note numbers for C4 to B4 (one octave)
private val whiteKeys = listOf(60, 62, 64, 65, 67, 69, 71)
private val blackKeys = listOf(61, 63, 66, 68, 70)
// Use immutable state
private var state = KeyboardState(initialOctave)
// Current octave with range validation
var octave: Int
get() = state.octave
set(value) {
updateOctave(value)
}
// Update state with functions that return new state
private fun updateOctave(newOctave: Int) {
state = state.copy(octave = newOctave.coerceIn(MIN_OCTAVE, MAX_OCTAVE))
requestUpdate()
}
// MIDI note numbers for one octave
private val whiteKeys = BASE_WHITE_KEYS
private val blackKeys = BASE_BLACK_KEYS
// Key dimensions
private val whiteKeyWidth = keyboardWidth / 7
private val blackKeyWidth = (keyboardWidth / 9)
private val blackKeyHeight = keyboardHeight * 60 / 100
private val whiteKeyWidth = keyboardWidth / WHITE_KEYS_PER_OCTAVE
private val blackKeyWidth = (keyboardWidth / BLACK_KEY_WIDTH_DIVISOR)
private val blackKeyHeight = keyboardHeight * BLACK_KEY_HEIGHT_PERCENTAGE / 100
// Calculate positions for black keys
private val blackKeyPositions = listOf(
@@ -57,28 +84,56 @@ class KeyboardComponent(
)
fun noteDown(midiNote: Int) {
pressedNotes.add(midiNote)
state = state.copy(pressedNotes = state.pressedNotes + midiNote)
onNoteDown(midiNote)
requestUpdate()
}
fun noteUp(midiNote: Int) {
pressedNotes.remove(midiNote)
state = state.copy(pressedNotes = state.pressedNotes - midiNote)
onNoteUp(midiNote)
requestUpdate()
}
private fun releaseAllNotes() {
// Create a copy of the set to avoid concurrent modification
val notesToRelease = pressedNotes.toSet()
val notesToRelease = state.pressedNotes.toSet()
for (note in notesToRelease) {
noteUp(note)
}
// Clear the set just to be safe
pressedNotes.clear()
// Just to be safe, clear all pressed notes
state = state.copy(pressedNotes = emptySet())
}
private fun getOctaveOffset(): Int = (octave - OCTAVE_BASE) * NOTES_PER_OCTAVE
private fun getMidiNoteFromMousePosition(x: Double, y: Double): Int? {
// Check if click is on a black key (black keys are on top of white keys)
if (y <= blackKeyHeight) {
for (j in 0 until BLACK_KEYS_PER_OCTAVE) {
if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) {
return blackKeys[j] + getOctaveOffset()
}
}
// If no black key was pressed, check for white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0 until WHITE_KEYS_PER_OCTAVE) {
return whiteKeys[keyIndex] + getOctaveOffset()
}
} else {
// If y > blackKeyHeight, it's definitely a white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0 until WHITE_KEYS_PER_OCTAVE) {
return whiteKeys[keyIndex] + getOctaveOffset()
}
}
return null
}
override fun HtmlBuilder.render() {
div(KeyboardCls.name) {
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + 60}px"
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + KEYBOARD_CONTROLS_HEIGHT}px"
onMouseLeaveFunction = { event ->
if (event is MouseEvent) {
@@ -87,14 +142,44 @@ class KeyboardComponent(
}
}
div(KeyboardTitleCls.name) {
// Show title of the keyboard
+title
}
div(KeyboardControlsCls.name) {
// Decrease octave button
div(OctaveButtonCls.name) {
style = "width: ${whiteKeyWidth}px"
+"<"
onMouseDownFunction = { event ->
if (event is MouseEvent) {
updateOctave(octave - 1)
event.preventDefault()
}
}
}
div(KeyboardOctaveCls.name) {
// Show current octave the piano is being played at
+"Octave: $octave"
// Title and octave display container
div(KeyboardInfoCls.name) {
div(KeyboardTitleCls.name) {
// Show title of the keyboard
+title
}
div(KeyboardOctaveCls.name) {
// Show current octave the piano is being played at
// minus one to keep it in line with custom notes ranges
+"Octave: ${octave -1}"
}
}
// Increase octave button
div(OctaveButtonCls.name) {
style = "width: ${whiteKeyWidth}px"
+">"
onMouseDownFunction = { event ->
if (event is MouseEvent) {
updateOctave(octave + 1)
event.preventDefault()
}
}
}
}
div(KeyboardKeysCls.name) {
@@ -107,111 +192,88 @@ class KeyboardComponent(
// Define mouse event handlers at the SVG level
onMouseDownFunction = { event ->
if (event is MouseEvent) {
val x = event.offsetX
val y = event.offsetY
// Check if click is on a black key (black keys are on top of white keys)
if (y <= blackKeyHeight) {
var blackKeyPressed = false
for (j in 0 until 5) {
if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) {
noteDown(blackKeys[j] + (octave - 4) * 12)
blackKeyPressed = true
break
}
}
// If no black key was pressed, check for white key
if (!blackKeyPressed) {
// Check if click is on a white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0..6) {
noteDown(whiteKeys[keyIndex] + (octave - 4) * 12)
}
}
} else {
// If y > blackKeyHeight, it's definitely a white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0..6) {
noteDown(whiteKeys[keyIndex] + (octave - 4) * 12)
}
}
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteDown(it) }
event.preventDefault()
}
}
onMouseUpFunction = { event ->
if (event is MouseEvent) {
val x = event.offsetX
val y = event.offsetY
// Check if release is on a black key (black keys are on top of white keys)
if (y <= blackKeyHeight) {
var blackKeyReleased = false
for (j in 0 until 5) {
if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) {
noteUp(blackKeys[j] + (octave - 4) * 12)
blackKeyReleased = true
break
}
}
// If no black key was released, check for white key
if (!blackKeyReleased) {
// Check if release is on a white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0..6) {
noteUp(whiteKeys[keyIndex] + (octave - 4) * 12)
}
}
} else {
// If y > blackKeyHeight, it's definitely a white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0..6) {
noteUp(whiteKeys[keyIndex] + (octave - 4) * 12)
}
}
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteUp(it) }
event.preventDefault()
}
}
// Draw white keys
for (i in 0 until 7) {
rect(
i * whiteKeyWidth,
0,
whiteKeyWidth,
keyboardHeight,
0,
WhiteKeyCls.name
)
}
this.renderWhiteKeys()
// Draw black keys
for (i in 0 until 5) {
rect(
blackKeyPositions[i],
0,
blackKeyWidth,
blackKeyHeight,
0,
BlackKeyCls.name
)
}
this.renderBlackKeys()
}
}
}
}
// Render white keys
private fun SVG.renderWhiteKeys() {
for (i in 0 until WHITE_KEYS_PER_OCTAVE) {
val midiNote = whiteKeys[i] + getOctaveOffset()
val isPressed = state.pressedNotes.contains(midiNote)
rect(
i * whiteKeyWidth,
0,
whiteKeyWidth,
keyboardHeight,
rounding,
if (isPressed) WhiteKeyPressedCls.name else WhiteKeyCls.name
)
}
}
// Render black keys
private fun SVG.renderBlackKeys() {
for (i in 0 until BLACK_KEYS_PER_OCTAVE) {
val midiNote = blackKeys[i] + getOctaveOffset()
val isPressed = state.pressedNotes.contains(midiNote)
rect(
blackKeyPositions[i],
0,
blackKeyWidth,
blackKeyHeight,
rounding,
if (isPressed) BlackKeyPressedCls.name else BlackKeyCls.name
)
}
}
companion object : CssId("keyboard") {
// CSS class names
object KeyboardCls : CssName()
object KeyboardControlsCls : CssName()
object KeyboardInfoCls : CssName()
object KeyboardTitleCls : CssName()
object KeyboardOctaveCls : CssName()
object KeyboardKeysCls : CssName()
object OctaveButtonCls : CssName()
object WhiteKeyCls : CssName()
object BlackKeyCls : CssName()
object WhiteKeyPressedCls : CssName()
object BlackKeyPressedCls : CssName()
// Constants
private const val OCTAVE_BASE = 5
private const val NOTES_PER_OCTAVE = 12
private const val BLACK_KEY_HEIGHT_PERCENTAGE = 60
private const val MIN_OCTAVE = 0
private const val MAX_OCTAVE = 9
private const val WHITE_KEYS_PER_OCTAVE = 7
private const val BLACK_KEYS_PER_OCTAVE = 5
private const val BLACK_KEY_WIDTH_DIVISOR = 9
private const val KEYBOARD_CONTROLS_HEIGHT = 60
// MIDI note numbers for C5 to B5 (one octave)
private val BASE_WHITE_KEYS = listOf(60, 62, 64, 65, 67, 69, 71) // C, D, E, F, G, A, B
private val BASE_BLACK_KEYS = listOf(61, 63, 66, 68, 70) // C#, D#, F#, G#, A#
init {
defineCss {
@@ -219,31 +281,57 @@ class KeyboardComponent(
position(Position.relative)
margin(5.px)
select(cls(KeyboardControlsCls)) {
position(Position.relative)
display(Display.flex)
flexDirection(FlexDirection.row)
justifyContent(JustifyContent.spaceBetween)
alignItems(AlignItems.center)
width(100.prc)
height(50.px)
marginBottom(10.px)
}
select(cls(KeyboardInfoCls)) {
display(Display.flex)
flexDirection(FlexDirection.column)
alignItems(AlignItems.center)
justifyContent(JustifyContent.center)
flex("1")
}
select(cls(KeyboardTitleCls)) {
position(Position.absolute)
width(100.px)
textAlign(TextAlign.center)
fontSize(1.2.rem)
color(Css.currentStyle.mainFontColor)
top(5.px)
left(0.px)
right(0.px)
}
select(cls(KeyboardOctaveCls)) {
position(Position.absolute)
width(100.px)
textAlign(TextAlign.center)
fontSize(1.0.rem)
color(Css.currentStyle.mainFontColor)
top(30.px)
left(0.px)
right(0.px)
marginTop(5.px)
}
select(cls(OctaveButtonCls)) {
height(50.px)
plain("background-color", Css.currentStyle.buttonBackgroundColor.toString())
color(Css.currentStyle.mainFontColor)
border("1px solid ${Css.currentStyle.buttonBorderColor}")
textAlign(TextAlign.center)
lineHeight(50.px)
fontSize(1.5.rem)
fontWeight(FontWeight.bold)
cursor("pointer")
plain("-webkit-touch-callout", "none")
plain("-webkit-user-select", "none")
plain("-moz-user-select", "none")
plain("-ms-user-select", "none")
plain("user-select", "none")
}
select(cls(KeyboardKeysCls)) {
position(Position.absolute)
top(60.px)
position(Position.relative)
}
}
@@ -253,11 +341,23 @@ class KeyboardComponent(
plain("stroke-width", "1")
}
select(cls(WhiteKeyPressedCls)) {
plain("fill", "#E6E6E6") // 10% darker than white
plain("stroke", "#000000")
plain("stroke-width", "1")
}
select(cls(BlackKeyCls)) {
plain("fill", "#000000")
plain("stroke", "#000000")
plain("stroke-width", "1")
}
select(cls(BlackKeyPressedCls)) {
plain("fill", "#333333") // 10% lighter than black
plain("stroke", "#000000")
plain("stroke-width", "1")
}
}
}
}

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