working version with new Kotlin frontend!!!

This commit is contained in:
2024-02-21 08:52:00 +08:00
parent d4306555df
commit b14862fc7d
20 changed files with 828 additions and 191 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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