Skip to content

Commit

Permalink
Merge branch 'release/0.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
DonRobo committed Jul 21, 2024
2 parents a89414a + f8e4bbf commit 4cb65c5
Show file tree
Hide file tree
Showing 20 changed files with 597 additions and 50 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
}

allprojects {
version = "0.2.2"
version = "0.3.0"

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion home-assistant-addon/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "Home Former"
version: "0.2.2"
version: "0.3.0"
slug: "homeformer"
description: >-
Former of homes
Expand Down
1 change: 1 addition & 0 deletions web/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {
implementation(project(":base"))
implementation(libs.bundles.ktor.server)
implementation(libs.bundles.exposed)
implementation(libs.shelly)
}

ktor {
Expand Down
14 changes: 11 additions & 3 deletions web/src/main/kotlin/at/robert/hf/Application.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package at.robert.hf

import at.robert.hf.plugins.configureConfigPage
import at.robert.hf.plugins.configureErrors
import at.robert.hf.plugins.configureStaticContent
import at.robert.hf.config.configureErrors
import at.robert.hf.config.configureStaticContent
import at.robert.hf.page.configureConfigPage
import at.robert.hf.page.configureHtmxPage
import at.robert.hf.page.configureShellyManager
import at.robert.hf.page.registerShellyPage
import io.ktor.server.application.*

fun main(args: Array<String>) {
Expand All @@ -13,5 +16,10 @@ fun Application.module() {
configureErrors()
configureStaticContent()
configureConfigPage()

configureShellyManager()
registerShellyPage()

configureHtmxPage()
}

18 changes: 18 additions & 0 deletions web/src/main/kotlin/at/robert/hf/config/Errors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package at.robert.hf.config

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*

fun Application.configureErrors() {
install(StatusPages) {
status(
HttpStatusCode.NotFound,
HttpStatusCode.MethodNotAllowed,
HttpStatusCode.InternalServerError
) { call, status ->
call.respondText(text = "${status.value}: ${status.description}", status = status)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package at.robert.hf.plugins
package at.robert.hf.config

import io.ktor.server.application.*
import io.ktor.server.http.content.*
Expand Down
10 changes: 10 additions & 0 deletions web/src/main/kotlin/at/robert/hf/htmx/HtmxComponent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package at.robert.hf.htmx

import io.ktor.http.*
import kotlinx.html.TagConsumer

interface HtmxComponent {
val id: String

suspend fun render(render: TagConsumer<*>, params: Parameters, hxCtx: HtmxContext)
}
6 changes: 6 additions & 0 deletions web/src/main/kotlin/at/robert/hf/htmx/HtmxContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package at.robert.hf.htmx

data class HtmxContext(
val route: String,
val component: String,
)
18 changes: 18 additions & 0 deletions web/src/main/kotlin/at/robert/hf/htmx/HtmxPage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package at.robert.hf.htmx

import kotlinx.html.TagConsumer
import kotlinx.html.h1

abstract class HtmxPage {
open suspend fun renderHeader(render: TagConsumer<*>) {
val title = title()
render.h1 {
+title
}
}

abstract suspend fun title(): String

abstract suspend fun components(): List<HtmxComponent>
open suspend fun getComponent(id: String): HtmxComponent? = components().find { it.id == id }
}
91 changes: 91 additions & 0 deletions web/src/main/kotlin/at/robert/hf/htmx/HtmxRenderer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package at.robert.hf.htmx

import at.robert.hf.respondHtmlBody
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.html.div
import kotlinx.html.id
import kotlinx.html.main
import kotlinx.html.stream.appendHTML
import kotlinx.html.unsafe

object HtmxRenderer {
private suspend fun renderHtmxPage(
page: HtmxPage,
currentRoute: String,
call: ApplicationCall
) = coroutineScope {
val title = page.title()

val headerD = async {
buildString {
page.renderHeader(appendHTML())
}
}

val renderedComponentsD = page.components().associateWith { comp ->
async {
buildString {
comp.render(appendHTML(), Parameters.Empty, HtmxContext(currentRoute, comp.id))
}
}
}

val header = headerD.await()
val renderedComponents = renderedComponentsD.mapValues { it.value.await() }

call.respondHtmlBody(title, includeHtmx = true) {
main {
unsafe {
raw(header)
}
renderedComponents.forEach { (component, content) ->
div {
id = component.id
unsafe {
raw(content)
}
}
}
}
}
}

private suspend fun renderHtmxComponent(
page: HtmxPage,
compId: String,
currentRoute: String,
call: ApplicationCall
) = coroutineScope {
val paramsD = async { call.receiveParameters() }
val componentD = async { page.getComponent(compId)!! }
val text = buildString {
val component = componentD.await()
component.render(appendHTML(), paramsD.await(), HtmxContext(currentRoute, component.id))
}
call.respond(TextContent(text, ContentType.Text.Html.withCharset(Charsets.UTF_8)))
}

fun registerPage(routing: Route, route: String, page: HtmxPage) {
registerPage(routing, route) { page }
}

fun registerPage(routing: Route, route: String, block: (Parameters) -> HtmxPage) {
routing.get(route) {
val page = block(call.parameters)
renderHtmxPage(page, call.request.path(), call)
}
routing.post(route) {
val page = block(call.parameters)
require(call.request.header("HX-Request") == "true")
val compId = call.request.header("HX-Target")!!
renderHtmxComponent(page, compId, call.request.path(), call)
}
}
}
15 changes: 15 additions & 0 deletions web/src/main/kotlin/at/robert/hf/htmx/SimpleHtmxComponent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package at.robert.hf.htmx

import io.ktor.http.*
import kotlinx.html.TagConsumer

class SimpleHtmxComponent(
override val id: String,
private val block: (TagConsumer<*>) -> Unit
) : HtmxComponent {
override suspend fun render(render: TagConsumer<*>, params: Parameters, hxCtx: HtmxContext) {
block(render)
}
}

fun simpleComponent(id: String, block: TagConsumer<*>.() -> Unit) = SimpleHtmxComponent(id, block)
30 changes: 30 additions & 0 deletions web/src/main/kotlin/at/robert/hf/htmx/util.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package at.robert.hf.htmx

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.html.*

val hxObjectMapper = jacksonObjectMapper()
fun Tag.hxPost(hxCtx: HtmxContext, vararg params: Pair<String, Any>) {
attributes["hx-post"] = hxCtx.route
attributes["hx-target"] = "#${hxCtx.component}"
attributes["hx-swap"] = "innerHTML"
attributes["hx-vals"] = params.toMap().let { hxObjectMapper.writeValueAsString(it) }
}

fun FlowContent.hxActionForm(
hxCtx: HtmxContext,
actionLabel: String,
block: FORM.() -> Unit
) {
form {
attributes["hx-post"] = hxCtx.route
attributes["hx-target"] = "#${hxCtx.component}"
attributes["hx-swap"] = "innerHTML"

block()
input(type = InputType.submit) {
value = actionLabel
}
}
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package at.robert.hf.plugins
package at.robert.hf.page

import at.robert.ConfigEvaluator
import at.robert.hf.ConfigFiles
import at.robert.hf.basePath
import at.robert.hf.respondHtmlBody
import at.robert.util.yamlObjectMapper
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.IntNode
Expand All @@ -10,7 +12,6 @@ import com.fasterxml.jackson.databind.node.TextNode
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
Expand Down Expand Up @@ -166,7 +167,7 @@ private fun FlowContent.renderJson(json: JsonNode) {
fun Application.configureConfigPage() {
routing {
get("/config") {
call.respondHtmlBody("Config", BODY::configList)
call.respondHtmlBody("Config", block = BODY::configList)
}
post("/config") {
uploadConfigFile(call)
Expand All @@ -176,26 +177,3 @@ fun Application.configureConfigPage() {
}
}
}

private suspend fun ApplicationCall.respondHtmlBody(title: String, block: BODY.() -> Unit) {
this.respondHtml {
head {
base {
href = basePath
}
title(title)
link(rel = "stylesheet", href = "https://unpkg.com/missing.css@1.1.2")
}
body {
block()
}
}
}

private val ApplicationCall.basePath: String
get() = (this.request.header("X-Ingress-Path") ?: "/").let {
if (it.endsWith("/"))
return it
else
return "$it/"
}
48 changes: 48 additions & 0 deletions web/src/main/kotlin/at/robert/hf/page/HtmxTestPage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package at.robert.hf.page

import at.robert.hf.htmx.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
import kotlinx.html.TagConsumer
import kotlinx.html.button
import kotlinx.html.div
import kotlinx.html.p

object HtmxTestPage : HtmxPage() {
object BasicHtmxComponent : HtmxComponent {
override val id: String
get() = "comp1"

override suspend fun render(render: TagConsumer<*>, params: Parameters, hxCtx: HtmxContext) {
render.apply {
p { +"This is a component" }
params.names().forEach {
div {
+"$it: ${params[it]}"
}
}
button {
hxPost(
hxCtx,
"greeting" to "Hello hxPost!",
)
+"Click me"
}
}
}
}

override suspend fun title() = "Htmx Test Page"

override suspend fun components(): List<HtmxComponent> {
return listOf(BasicHtmxComponent)
}

}

fun Application.configureHtmxPage() {
routing {
HtmxRenderer.registerPage(this, "/htmx", HtmxTestPage)
}
}
Loading

0 comments on commit 4cb65c5

Please sign in to comment.