working version with new Kotlin frontend!!!
This commit is contained in:
@@ -1,11 +1,54 @@
|
||||
plugins {
|
||||
alias(catalog.plugins.kotlin.multiplatform)
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "1.9.22"
|
||||
}
|
||||
|
||||
|
||||
|
||||
configurations {
|
||||
tar {
|
||||
canBeConsumed = true
|
||||
canBeResolved = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation catalog.klevtree
|
||||
implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-serialization-json', version: '1.6.2'
|
||||
// implementation(npm("bootstrap", "5.3.2"))
|
||||
// implementation(npm("bootstrap-icons", "1.11.3"))
|
||||
// implementation(npm("style-loader", "3.3.4"))
|
||||
// implementation(npm("css-loader", "6.10.0"))
|
||||
}
|
||||
}
|
||||
}
|
||||
js(IR) {
|
||||
browser {
|
||||
runTask {
|
||||
sourceMaps = true
|
||||
devServer.port = 8080
|
||||
}
|
||||
webpackTask {
|
||||
sourceMaps = false
|
||||
}
|
||||
}
|
||||
binaries.executable()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Provider<Tar> distTarTaskProvider = tasks.register('distTar', Tar) { tar ->
|
||||
from(tasks.named('jsBrowserDistribution'))
|
||||
}
|
||||
|
||||
|
||||
def distributionTaskProvider = tasks.named("jsBrowserDistribution")
|
||||
|
||||
artifacts {
|
||||
tar(distributionTaskProvider)
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package net.woggioni.jpacrepo.common
|
||||
|
||||
object JpacrepoCommons {
|
||||
private val hexArray = "0123456789ABCDEF".toCharArray()
|
||||
|
||||
fun ByteArray.toHexString(): String {
|
||||
val hexChars = CharArray(this.size * 2)
|
||||
for (j in this.indices) {
|
||||
val v = this[j].toInt() and 0xFF
|
||||
hexChars[j * 2] = hexArray[v ushr 4]
|
||||
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
|
||||
}
|
||||
return hexChars.concatToString()
|
||||
}
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
package net.woggioni.jpacrepo
|
||||
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.startCoroutine
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.js.Promise
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.events.Event
|
||||
|
||||
object HtmlUtils {
|
||||
fun Element.removeChildren() {
|
||||
while (true) {
|
||||
removeChild(firstChild ?: break)
|
||||
}
|
||||
}
|
||||
|
||||
fun Element.on(eventName : String, eventListener : (Event) -> Unit) {
|
||||
addEventListener(eventName, eventListener)
|
||||
}
|
||||
|
||||
fun launch(block: suspend () -> Unit) {
|
||||
block.startCoroutine(object : Continuation<Unit> {
|
||||
override val context: CoroutineContext get() = EmptyCoroutineContext
|
||||
override fun resumeWith(result: Result<Unit>) {}
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
|
||||
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
||||
}
|
||||
}
|
||||
|
||||
class HtmlBuilder private constructor(private val doc : Document, val el: Element) {
|
||||
|
||||
companion object {
|
||||
fun <T> of(doc : Document, el: Element, cb : HtmlBuilder.(el : Element) -> T) : T {
|
||||
return HtmlBuilder(doc, el).cb(el)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> dfd(
|
||||
name : String,
|
||||
attrs : Map<String, String>,
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T {
|
||||
val child = doc.createElement(name)
|
||||
for((key, value) in attrs) {
|
||||
child.setAttribute(key, value)
|
||||
}
|
||||
el.appendChild(child)
|
||||
return HtmlBuilder(doc, child).cb(child)
|
||||
}
|
||||
|
||||
fun <T> html(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("html", attrs, cb)
|
||||
fun <T> head(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("head", attrs, cb)
|
||||
fun <T> body(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("body", attrs, cb)
|
||||
|
||||
fun <T> use(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("use", attrs, cb)
|
||||
fun <T> svg(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("svg", attrs, cb)
|
||||
fun <T> div(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("div", attrs, cb)
|
||||
fun <T> header(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("header", attrs, cb)
|
||||
fun <T> main(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("main", attrs, cb)
|
||||
fun <T> footer(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("footer", attrs, cb)
|
||||
fun <T> a(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("a", attrs, cb)
|
||||
fun <T> meta(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("meta", attrs, cb)
|
||||
fun <T> script(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("script", attrs, cb)
|
||||
fun <T> link(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("link", attrs, cb)
|
||||
fun <T> title(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("title", attrs, cb)
|
||||
fun <T> p(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("p", attrs, cb)
|
||||
fun <T> span(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("span", attrs, cb)
|
||||
fun <T> i(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("i", attrs, cb)
|
||||
fun <T> del(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("del", attrs, cb)
|
||||
fun <T> s(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("s", attrs, cb)
|
||||
fun <T> ins(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("ins", attrs, cb)
|
||||
fun <T> u(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("u", attrs, cb)
|
||||
fun <T> b(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("b", attrs, cb)
|
||||
fun <T> small(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("small", attrs, cb)
|
||||
fun <T> strong(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("strong", attrs, cb)
|
||||
fun <T> em(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("em", attrs, cb)
|
||||
fun <T> mark(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("mark", attrs, cb)
|
||||
fun <T> obj(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("object", attrs, cb)
|
||||
fun <T> h1(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h1", attrs, cb)
|
||||
fun <T> h2(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h2", attrs, cb)
|
||||
fun <T> h3(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h3", attrs, cb)
|
||||
fun <T> h4(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h4", attrs, cb)
|
||||
fun <T> h5(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h5", attrs, cb)
|
||||
fun <T> h6(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h6", attrs, cb)
|
||||
fun <T> table(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("table", attrs, cb)
|
||||
fun <T> thead(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("thead", attrs, cb)
|
||||
fun <T> tbody(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("tbody", attrs, cb)
|
||||
fun <T> tfoot(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("tfoot", attrs, cb)
|
||||
fun <T> tr(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("tr", attrs, cb)
|
||||
fun <T> th(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("th", attrs, cb)
|
||||
fun <T> td(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("td", attrs, cb)
|
||||
fun <T> ol(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("ol", attrs, cb)
|
||||
fun <T> ul(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("ul", attrs, cb)
|
||||
fun <T> li(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("li", attrs, cb)
|
||||
fun <T> img(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("img", attrs, cb)
|
||||
fun <T> form(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("form", attrs, cb)
|
||||
fun <T> label(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("label", attrs, cb)
|
||||
fun <T> button(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("button", attrs, cb)
|
||||
fun <T> input(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("input", attrs, cb)
|
||||
fun <T> select(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("select", attrs, cb)
|
||||
fun <T> option(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("option", attrs, cb)
|
||||
fun <T> meter(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("meter", attrs, cb)
|
||||
fun <T> nav(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("nav", attrs, cb)
|
||||
fun <T> menu(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("menu", attrs, cb)
|
||||
|
||||
|
||||
fun classes(vararg classes : String) {
|
||||
for(cls in classes) el.classList.add(cls)
|
||||
}
|
||||
|
||||
fun attr(key: String, value : String) {
|
||||
el.setAttribute(key, value)
|
||||
}
|
||||
|
||||
fun text(txt : String) {
|
||||
el.appendChild(doc.createTextNode(txt))
|
||||
}
|
||||
|
||||
fun on(eventName : String, cb: (Event) -> Unit) {
|
||||
el.addEventListener(eventName, cb)
|
||||
}
|
||||
}
|
@@ -0,0 +1,510 @@
|
||||
package net.woggioni.jpacrepo
|
||||
|
||||
import kotlin.js.Promise
|
||||
import kotlin.random.Random
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.woggioni.jpacrepo.HtmlUtils.await
|
||||
import net.woggioni.jpacrepo.HtmlUtils.launch
|
||||
import net.woggioni.jpacrepo.HtmlUtils.on
|
||||
import net.woggioni.jpacrepo.HtmlUtils.removeChildren
|
||||
import net.woggioni.jpacrepo.common.JpacrepoCommons.toHexString
|
||||
import net.woggioni.klevtree.LevTrie
|
||||
import org.w3c.dom.CustomEvent
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLFormElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.HTMLSelectElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.get
|
||||
|
||||
private val UNITS_OF_MEASURE = arrayOf("B", "KiB", "MiB", "GiB", "TiB", "PiB")
|
||||
fun Long.toHumanReadableByteSize(): String {
|
||||
var i = 0
|
||||
var multiple = 1
|
||||
while (i < UNITS_OF_MEASURE.size) {
|
||||
val label = UNITS_OF_MEASURE[i]
|
||||
val newMultiple = multiple * 1024;
|
||||
if (this > multiple && (i + 1 > UNITS_OF_MEASURE.size || this < newMultiple)) {
|
||||
return (this.toDouble() / multiple).asDynamic().toPrecision(3) + " " + label;
|
||||
}
|
||||
i += 1
|
||||
multiple = newMultiple;
|
||||
}
|
||||
throw Error("This should never happen")
|
||||
}
|
||||
|
||||
typealias PkgMap = Map<String, Map<String, Map<String, Map<CompressionFormat, PkgTuple>>>>
|
||||
|
||||
private fun createPackageArchive(): Promise<PackageArchive> {
|
||||
return window.fetch(SERVER_PREFIX + "api/pkg/map")
|
||||
.then { it.text() }
|
||||
.then { Json.decodeFromString<PkgMap>(it) }
|
||||
.then(::PackageArchive)
|
||||
}
|
||||
|
||||
class PackageArchive(val pkgMap: PkgMap) {
|
||||
val tries: Map<String, LevTrie>
|
||||
|
||||
init {
|
||||
tries = pkgMap.asSequence().map {
|
||||
it.key to LevTrie().apply {
|
||||
algorithm = LevTrie.Algorithm.DAMERAU_LEVENSHTEIN
|
||||
for (pkgName in it.value.keys) {
|
||||
add(pkgName)
|
||||
}
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
fun searchByArch(arch: String, needle: String, maxResult: Int): List<String> {
|
||||
return tries[arch]?.let {
|
||||
it.fuzzySearch(needle, maxResult)
|
||||
.asSequence()
|
||||
.sortedBy { it.second }
|
||||
.map { it.first }
|
||||
.toList()
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private const val REFRESH_TABLE_EVENT_NAME = "refreshTable"
|
||||
private const val REFRESH_TABLE_ROW_EVENT_NAME = "refreshTableRow"
|
||||
private const val REFRESH_SELECTED_PACKAGES_EVENT_NAME = "refreshSelectedPackages"
|
||||
//private const val SERVER_PREFIX = "https://woggioni.net/jpacrepo/"
|
||||
private const val SERVER_PREFIX = ""
|
||||
|
||||
private fun fireTableRefreshEvent(el: Element) {
|
||||
val evt = CustomEvent(
|
||||
REFRESH_TABLE_EVENT_NAME
|
||||
).apply {
|
||||
initCustomEvent(
|
||||
REFRESH_TABLE_EVENT_NAME,
|
||||
bubbles = true,
|
||||
cancelable = true,
|
||||
null
|
||||
)
|
||||
}
|
||||
el.dispatchEvent(evt)
|
||||
}
|
||||
|
||||
private fun fireTableRowRefreshEvent(el: Element) {
|
||||
val evt = CustomEvent(
|
||||
REFRESH_TABLE_ROW_EVENT_NAME
|
||||
).apply {
|
||||
initCustomEvent(
|
||||
REFRESH_TABLE_ROW_EVENT_NAME,
|
||||
bubbles = false,
|
||||
cancelable = true,
|
||||
null
|
||||
)
|
||||
}
|
||||
el.dispatchEvent(evt)
|
||||
}
|
||||
|
||||
private fun fireSelectedPackagesRefreshEvent(el: Element) {
|
||||
val evt = CustomEvent(
|
||||
REFRESH_SELECTED_PACKAGES_EVENT_NAME
|
||||
).apply {
|
||||
initCustomEvent(
|
||||
REFRESH_SELECTED_PACKAGES_EVENT_NAME,
|
||||
bubbles = true,
|
||||
cancelable = true,
|
||||
null
|
||||
)
|
||||
}
|
||||
el.dispatchEvent(evt)
|
||||
}
|
||||
|
||||
var pkgArchive: Promise<PackageArchive> = createPackageArchive()
|
||||
var selectedArchitecture: String? = null
|
||||
val selectedPackages: MutableSet<PkgTuple> = mutableSetOf()
|
||||
|
||||
suspend fun buildTable(root: Element, searchTerm: String, commonElement: Element) {
|
||||
selectedArchitecture?.let { arch ->
|
||||
val archive = pkgArchive.await()
|
||||
val results = archive.searchByArch(arch, searchTerm, 10)
|
||||
val children = root.children
|
||||
while (children.length > 0) {
|
||||
children[0]!!.remove()
|
||||
}
|
||||
HtmlBuilder.of(document, root) {
|
||||
table {
|
||||
classes("table", "table-striped", "pkgtable")
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
|
||||
}
|
||||
th {
|
||||
text("Name")
|
||||
}
|
||||
th {
|
||||
text("Version")
|
||||
}
|
||||
th {
|
||||
text("Compression format")
|
||||
}
|
||||
th {
|
||||
text("Filename")
|
||||
}
|
||||
th {
|
||||
text("Size")
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for (pkgName in results) {
|
||||
val packages = archive.pkgMap[arch]?.let { it[pkgName] }
|
||||
var selectedPackage: Pair<PkgId, PkgTuple>? = let {
|
||||
packages?.iterator()?.next()?.let {
|
||||
val version = it.key
|
||||
it.value.iterator().next().let {
|
||||
val compressionFormat = it.key
|
||||
PkgId(arch, pkgName, version, compressionFormat) to it.value
|
||||
}
|
||||
}
|
||||
}
|
||||
var selectedVersion = packages?.keys?.first()
|
||||
tr { row ->
|
||||
td {
|
||||
i {
|
||||
classes("bi-plus-circle")
|
||||
attr("style", "font-size: 1.5rem; color: grey;")
|
||||
on("click") {
|
||||
selectedPackage?.let {
|
||||
selectedPackages.add(it.second)
|
||||
}
|
||||
fireSelectedPackagesRefreshEvent(commonElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
text(pkgName)
|
||||
}
|
||||
td {
|
||||
packages?.keys?.let { keys ->
|
||||
select {
|
||||
classes("form-select")
|
||||
for (version in keys) {
|
||||
option {
|
||||
attr("value", version)
|
||||
text(version)
|
||||
}
|
||||
}
|
||||
on("input") { evt: Event ->
|
||||
val htmlInputEvent = evt.target as? HTMLSelectElement
|
||||
selectedVersion = htmlInputEvent?.value
|
||||
fireTableRowRefreshEvent(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val getFirstAvailableCompressionFormat = {
|
||||
val availablePackages = packages
|
||||
?.let { it[selectedVersion] }
|
||||
availablePackages?.keys?.first()
|
||||
}
|
||||
var selectedCompressionFormat = getFirstAvailableCompressionFormat()
|
||||
|
||||
fun HtmlBuilder.createCompressionFormatDropdown() {
|
||||
val availablePackages = packages
|
||||
?.let { it[selectedVersion] }
|
||||
availablePackages?.keys?.let { compressionFormats ->
|
||||
select {
|
||||
classes("form-select")
|
||||
for (compressionFormat in compressionFormats) {
|
||||
option {
|
||||
attr("value", compressionFormat.name)
|
||||
text(compressionFormat.toString())
|
||||
}
|
||||
}
|
||||
on("input") { evt: Event ->
|
||||
val htmlInputEvent = evt.target as? HTMLSelectElement
|
||||
selectedCompressionFormat = htmlInputEvent
|
||||
?.value
|
||||
?.let(CompressionFormat::valueOf)
|
||||
fireTableRowRefreshEvent(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val compressionFormatCell = td {
|
||||
createCompressionFormatDropdown()
|
||||
el
|
||||
}
|
||||
val fileNameCell = td {
|
||||
selectedPackage?.let {
|
||||
text(it.second.fileName)
|
||||
}
|
||||
el
|
||||
}
|
||||
val sizeCell = td {
|
||||
selectedPackage?.let {
|
||||
text(it.second.size.toHumanReadableByteSize())
|
||||
}
|
||||
el
|
||||
}
|
||||
on(REFRESH_TABLE_ROW_EVENT_NAME) { evt ->
|
||||
selectedCompressionFormat = getFirstAvailableCompressionFormat()
|
||||
compressionFormatCell.removeChildren()
|
||||
HtmlBuilder.of(document, compressionFormatCell) {
|
||||
createCompressionFormatDropdown()
|
||||
}
|
||||
selectedPackage = archive.pkgMap[arch]?.let {
|
||||
it[pkgName]
|
||||
}?.let {
|
||||
it[selectedVersion]
|
||||
}?.let {
|
||||
it[selectedCompressionFormat]
|
||||
}?.let { pkgTuple ->
|
||||
PkgId(arch, pkgName, selectedVersion!!, selectedCompressionFormat!!) to pkgTuple
|
||||
}
|
||||
selectedPackage?.let { pair ->
|
||||
fileNameCell.textContent = pair.second.fileName
|
||||
sizeCell.textContent = pair.second.size.toHumanReadableByteSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun HtmlBuilder.buildPackageSearchSection(commonElement: Element) {
|
||||
val root = el
|
||||
var selectedPackageName: String? = null
|
||||
div {
|
||||
classes("row", "g-5")
|
||||
div {
|
||||
classes("col-md12")
|
||||
form {
|
||||
classes("needs-validation")
|
||||
div {
|
||||
classes("row", "g-3")
|
||||
label {
|
||||
classes("col-sm-2", "col-form-label")
|
||||
attr("for", "packageName")
|
||||
text("Search package")
|
||||
}
|
||||
div {
|
||||
classes("col-sm-10", "col-form-label")
|
||||
input { inputElement ->
|
||||
attr("id", "packageName")
|
||||
attr("type", "text")
|
||||
attr("placeholder", "")
|
||||
classes("form-control")
|
||||
on("input") {
|
||||
(it.target as? HTMLInputElement)?.value?.let { packageName ->
|
||||
selectedPackageName = packageName
|
||||
fireTableRefreshEvent(
|
||||
inputElement
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
classes("d-flex")
|
||||
text("Architecture:")
|
||||
div {
|
||||
classes("form-check")
|
||||
pkgArchive.then { pkgArchive ->
|
||||
for (arch in pkgArchive.pkgMap.keys) {
|
||||
val id = Random.Default.nextBytes(4).toHexString()
|
||||
div {
|
||||
classes("form-check", "form-check-inline")
|
||||
input { inputElement ->
|
||||
attr("id", id)
|
||||
attr("type", "radio")
|
||||
attr("autocomplete", "off")
|
||||
attr("name", "arch")
|
||||
classes("form-check-input")
|
||||
on("input") { inputEvent ->
|
||||
(inputEvent.target as? HTMLInputElement)
|
||||
?.takeIf(HTMLInputElement::checked)
|
||||
?.let {
|
||||
selectedArchitecture = arch
|
||||
}
|
||||
(inputEvent.target as? HTMLInputElement)?.value?.let {
|
||||
fireTableRefreshEvent(inputElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
label {
|
||||
classes("form-check-label")
|
||||
attr("for", id)
|
||||
text(arch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div { table ->
|
||||
classes("row", "align-items-md-stretch")
|
||||
root.on(REFRESH_TABLE_EVENT_NAME) { evt ->
|
||||
launch {
|
||||
(evt as? CustomEvent)?.let { pnuv ->
|
||||
table.let { t ->
|
||||
selectedPackageName?.let { pkgName ->
|
||||
t.removeChildren()
|
||||
buildTable(t, pkgName, commonElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main(vararg args: String) {
|
||||
HtmlBuilder.of(document, document.body as HTMLElement) {
|
||||
main { main ->
|
||||
div {
|
||||
classes("container", "py-4")
|
||||
div {
|
||||
classes("p-5", "mb-4", "bg-body-tertiary", "rounded-3")
|
||||
div {
|
||||
classes("container-fluid", "py-5")
|
||||
h1 {
|
||||
classes("display-5", "fw-bold")
|
||||
text("Jpacrepo")
|
||||
}
|
||||
p {
|
||||
classes("col-md-8", "fs-4")
|
||||
text("Personal archlinux package repository")
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
classes("container")
|
||||
div {
|
||||
val commonElement = el
|
||||
classes("row", "align-items-start")
|
||||
div {
|
||||
classes("col-3")
|
||||
div {
|
||||
classes("card")
|
||||
div {
|
||||
classes("card-header")
|
||||
span {
|
||||
classes("fs-5", "fw-semibold")
|
||||
text("Package Cart")
|
||||
}
|
||||
}
|
||||
fun HtmlBuilder.createSelectedPackageList() {
|
||||
if (selectedPackages.isNotEmpty()) {
|
||||
div {
|
||||
classes("card-body")
|
||||
ul {
|
||||
classes("list-group")
|
||||
for (selectedPackage in selectedPackages) {
|
||||
li {
|
||||
classes(
|
||||
"list-group-item",
|
||||
"d-flex",
|
||||
"justify-content-between",
|
||||
"align-items-start"
|
||||
)
|
||||
text(selectedPackage.fileName)
|
||||
i {
|
||||
classes("bi-x-lg")
|
||||
attr("style", "font-size: 1.5rem; color: grey;")
|
||||
on("click") {
|
||||
selectedPackages.remove(selectedPackage)
|
||||
fireSelectedPackagesRefreshEvent(commonElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
classes("btn", "btn-primary", "w-100")
|
||||
el.asDynamic().style.marginTop = "10px"
|
||||
val totalSize = selectedPackages
|
||||
.asSequence()
|
||||
.map(PkgTuple::size)
|
||||
.fold(0L, Long::plus)
|
||||
text("Download (${totalSize.toHumanReadableByteSize()})")
|
||||
on("click") {
|
||||
val temporaryForm =
|
||||
document.createElement("form") as HTMLFormElement
|
||||
temporaryForm.method = "post"
|
||||
temporaryForm.action =
|
||||
SERVER_PREFIX + "api/pkg/downloadTar"
|
||||
temporaryForm.asDynamic().style.display = "none"
|
||||
val textField = document.createElement("input") as HTMLInputElement
|
||||
textField.setAttribute("name", "pkgs")
|
||||
textField.value = selectedPackages
|
||||
.asSequence()
|
||||
.map(PkgTuple::fileName)
|
||||
.joinToString(" ")
|
||||
temporaryForm.appendChild(textField)
|
||||
el.appendChild(temporaryForm)
|
||||
temporaryForm.submit()
|
||||
el.removeChild(temporaryForm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
createSelectedPackageList()
|
||||
commonElement.on(REFRESH_SELECTED_PACKAGES_EVENT_NAME) {
|
||||
el.removeChildren()
|
||||
createSelectedPackageList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
classes("col-9")
|
||||
buildPackageSearchSection(commonElement)
|
||||
}
|
||||
el
|
||||
}
|
||||
}
|
||||
footer {
|
||||
classes("pt-3", "mt-4", "text-body-secondary", "border-top")
|
||||
text("©2024")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class CompressionFormat(private val value: String) {
|
||||
XZ("xz"), GZIP("gz"), Z_STANDARD("zst");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
data class PkgId(
|
||||
var arch: String,
|
||||
var name: String,
|
||||
var version: String,
|
||||
var compressionFormat: CompressionFormat,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PkgTuple(
|
||||
var md5sum: String,
|
||||
var fileName: String,
|
||||
var size: Long,
|
||||
)
|
||||
|
11
jpacrepo-frontend/src/jsMain/resources/css/jpacrepo.css
Normal file
11
jpacrepo-frontend/src/jsMain/resources/css/jpacrepo.css
Normal file
@@ -0,0 +1,11 @@
|
||||
svg.bi-plus-circle {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.table > tbody:nth-child(2) > tr > td:nth-child(1) > i {
|
||||
visibility : hidden;
|
||||
}
|
||||
|
||||
.table > tbody:nth-child(2) > tr:hover > td:nth-child(1) > i {
|
||||
visibility : visible;
|
||||
}
|
18
jpacrepo-frontend/src/jsMain/resources/index.html
Normal file
18
jpacrepo-frontend/src/jsMain/resources/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="stylesheet" href="css/jpacrepo.css"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin="anonymous"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="jpacrepo-frontend.js" defer="true"></script>
|
||||
<title>Hello World!</title>
|
||||
</head>
|
||||
</html>
|
@@ -1,70 +0,0 @@
|
||||
package net.woggioni.jpacrepo
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
|
||||
|
||||
class HtmlBuilder private constructor(private val doc : Document, val el: Element) {
|
||||
|
||||
companion object {
|
||||
fun <T> of(doc : Document, el: Element, cb : HtmlBuilder.(el : Element) -> T) : T {
|
||||
return HtmlBuilder(doc, el).cb(el)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> dfd(
|
||||
name : String,
|
||||
attrs : Map<String, String>,
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T {
|
||||
val child = doc.createElement(name)
|
||||
for((key, value) in attrs) {
|
||||
child.setAttribute(key, value)
|
||||
}
|
||||
el.appendChild(child)
|
||||
return HtmlBuilder(doc, child).cb(child)
|
||||
}
|
||||
|
||||
fun <T> html(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("html", attrs, cb)
|
||||
fun <T> head(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("head", attrs, cb)
|
||||
fun <T> body(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("body", attrs, cb)
|
||||
|
||||
fun <T> div(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("div", attrs, cb)
|
||||
fun <T> header(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("header", attrs, cb)
|
||||
fun <T> main(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("main", attrs, cb)
|
||||
fun <T> footer(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("footer", attrs, cb)
|
||||
fun <T> a(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("a", attrs, cb)
|
||||
fun <T> meta(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("meta", attrs, cb)
|
||||
fun <T> script(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("script", attrs, cb)
|
||||
fun <T> link(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("link", attrs, cb)
|
||||
fun <T> title(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("title", attrs, cb)
|
||||
fun <T> p(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("p", attrs, cb)
|
||||
|
||||
fun <T> h1(attrs : Map<String, String> = emptyMap(),
|
||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h1", attrs, cb)
|
||||
|
||||
|
||||
fun classes(vararg classes : String) {
|
||||
for(cls in classes) el.classList.add(cls)
|
||||
}
|
||||
|
||||
fun attr(key: String, value : String) {
|
||||
el.setAttribute(key, value)
|
||||
}
|
||||
|
||||
fun text(txt : String) {
|
||||
el.textContent = txt
|
||||
}
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package net.woggioni.jpacrepo
|
||||
|
||||
import kotlinx.browser.document
|
||||
import net.woggioni.jpacrepo.Component.container
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
|
||||
object Component {
|
||||
val container = document.getElementById("container") as HTMLElement
|
||||
|
||||
fun sayHelloViaDom() {
|
||||
container.textContent = "Hello, DOM! Kotlin is writing…"
|
||||
}
|
||||
}
|
||||
|
||||
fun main(vararg args: String) {
|
||||
// println("JavaScript generated through Kotlin")
|
||||
//
|
||||
// Component.sayHelloViaDom()
|
||||
// sayHelloViaJsConsole()
|
||||
// sayHelloViaInlinedJavaScript()
|
||||
|
||||
HtmlBuilder.of(document, document.body as HTMLElement) {
|
||||
classes("d-flex", "h-100", "text-center", "text-white", "bg-dark")
|
||||
div {
|
||||
classes("cover-container", "d-flex", "w-100", "h-100", "p-3", "mx-auto", "flex-column")
|
||||
|
||||
}
|
||||
header {
|
||||
classes("mb-auto")
|
||||
}
|
||||
main {
|
||||
classes("px-3")
|
||||
h1 {
|
||||
text("Cover your page.")
|
||||
}
|
||||
p {
|
||||
classes("lead")
|
||||
text("Cover is a one-page template for building simple and beautiful home pages. Download, edit the text, and add your own fullscreen background photo to make it your own.")
|
||||
}
|
||||
p {
|
||||
classes("lead")
|
||||
a {
|
||||
classes("btn", "btn-lg", "btn-secondary", "fw-bold", "border-white", "bg-white")
|
||||
text("Learn more")
|
||||
}
|
||||
}
|
||||
}
|
||||
footer {
|
||||
classes("mt-auto", "text-white-50")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sayHelloViaJsConsole() {
|
||||
console.log("Hello from `console.log()`!")
|
||||
}
|
||||
|
||||
private fun sayHelloViaInlinedJavaScript() {
|
||||
js("document.writeln('Hello, from inlined JavaScript in Kotlin!')")
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="jpacrepo-frontend.js" defer="true"></script>
|
||||
<title>Hello World!</title>
|
||||
</html>
|
Reference in New Issue
Block a user