Various improvements
authorLanius Trolling <lanius@laniustrolling.dev>
Sun, 14 May 2023 13:49:08 +0000 (09:49 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Sun, 14 May 2023 13:49:08 +0000 (09:49 -0400)
.idea/kotlinc.xml
build.gradle.kts
src/main/kotlin/info/mechyrdia/Factbooks.kt
src/main/kotlin/info/mechyrdia/data/view_comments.kt
src/main/resources/static/init.js
src/main/resources/static/style.css

index 2b8a50fc21f55078d651117a958476d2397572e5..0fc3113136756acc4597486432227a66d5ebe736 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="KotlinJpsPluginSettings">
-    <option name="version" value="1.8.0" />
+    <option name="version" value="1.8.10" />
   </component>
 </project>
\ No newline at end of file
index 6a6f8f7ca3cd4077fae2091cee89be62f41891c8..50e2cc0cce7f9db21c7653c76c2e7b702a2d6111 100644 (file)
@@ -1,9 +1,8 @@
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
 import com.nixxcode.jvmbrotli.common.BrotliLoader
 import com.nixxcode.jvmbrotli.enc.BrotliOutputStream
 import com.nixxcode.jvmbrotli.enc.Encoder
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import java.security.MessageDigest
-import java.util.*
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executors
 import java.util.zip.GZIPOutputStream
@@ -25,8 +24,8 @@ buildscript {
 
 plugins {
        java
-       kotlin("jvm") version "1.8.0"
-       kotlin("plugin.serialization") version "1.8.0"
+       kotlin("jvm") version "1.8.10"
+       kotlin("plugin.serialization") version "1.8.10"
        id("com.github.johnrengelman.shadow") version "7.1.2"
        application
 }
@@ -45,16 +44,16 @@ dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.1")
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1")
        
-       implementation("io.ktor:ktor-server-core-jvm:2.2.4")
-       implementation("io.ktor:ktor-server-netty-jvm:2.2.4")
+       implementation("io.ktor:ktor-server-core-jvm:2.3.0")
+       implementation("io.ktor:ktor-server-netty-jvm:2.3.0")
        
-       implementation("io.ktor:ktor-server-call-id:2.2.4")
-       implementation("io.ktor:ktor-server-call-logging:2.2.4")
-       implementation("io.ktor:ktor-server-conditional-headers:2.2.4")
-       implementation("io.ktor:ktor-server-forwarded-header:2.2.4")
-       implementation("io.ktor:ktor-server-html-builder:2.2.4")
-       implementation("io.ktor:ktor-server-sessions-jvm:2.2.4")
-       implementation("io.ktor:ktor-server-status-pages:2.2.4")
+       implementation("io.ktor:ktor-server-call-id:2.3.0")
+       implementation("io.ktor:ktor-server-call-logging:2.3.0")
+       implementation("io.ktor:ktor-server-conditional-headers:2.3.0")
+       implementation("io.ktor:ktor-server-forwarded-header:2.3.0")
+       implementation("io.ktor:ktor-server-html-builder:2.3.0")
+       implementation("io.ktor:ktor-server-sessions-jvm:2.3.0")
+       implementation("io.ktor:ktor-server-status-pages:2.3.0")
        
        implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.0")
        
@@ -98,8 +97,6 @@ tasks.named<Copy>("processResources") {
                val pool = Executors.newWorkStealingPool()
                
                val encoderParams = if (BrotliLoader.isBrotliAvailable()) Encoder.Parameters().setQuality(8) else null
-               val base64 = Base64.getUrlEncoder()
-               val hashDigest = ThreadLocal.withInitial { MessageDigest.getInstance("SHA-256") }
                
                val resourceTree = fileTree(mapOf("dir" to outputs.files.asPath + "/static/", "exclude" to listOf("*.gz", "*.br", "*.sha256")))
                val countDownLatch = CountDownLatch(resourceTree.count())
@@ -107,9 +104,6 @@ tasks.named<Copy>("processResources") {
                for (file in resourceTree) {
                        pool.execute {
                                val bytes = file.readBytes()
-                               val hashFile = File("${file.absolutePath}.sha256").bufferedWriter()
-                               hashFile.write(base64.encodeToString(hashDigest.get().digest(bytes)).trimEnd('='))
-                               hashFile.close()
                                
                                val result = File("${file.absolutePath}.gz").outputStream()
                                val gzipStream = GZIPOutputStream(result)
@@ -132,6 +126,11 @@ tasks.named<Copy>("processResources") {
        }
 }
 
+tasks.named<ShadowJar>("shadowJar") {
+       mergeServiceFiles()
+       exclude { it.name == "module-info.class" }
+}
+
 application {
        mainClass.set("info.mechyrdia.Factbooks")
 }
index b9157341d87b9e991e85600ffe0bffb617fbac8b..6ab8f1fb44549e203b8bdea86753645e7dcaa79f 100644 (file)
@@ -23,7 +23,6 @@ import io.ktor.server.response.*
 import io.ktor.server.routing.*
 import io.ktor.server.sessions.*
 import io.ktor.server.sessions.serialization.*
-import io.ktor.server.util.*
 import io.ktor.util.*
 import org.slf4j.event.Level
 import java.io.File
@@ -31,12 +30,6 @@ import java.io.IOException
 import java.io.InputStream
 import java.util.concurrent.atomic.AtomicLong
 
-object ResourceLoader {
-       fun getResource(resource: String): InputStream? = javaClass.getResourceAsStream(resource)
-       
-       val SHA256AttributeKey = AttributeKey<String>("SHA256Hash")
-}
-
 lateinit var application: Application
        private set
 
@@ -73,14 +66,6 @@ fun Application.factbooks() {
                }
        }
        
-       install(ConditionalHeaders) {
-               version { call, _ ->
-                       call.attributes.getOrNull(ResourceLoader.SHA256AttributeKey)?.let { hash ->
-                               listOf(EntityTagVersion(hash))
-                       }.orEmpty()
-               }
-       }
-       
        install(Sessions) {
                cookie("USER_SESSION", SessionStorageMongoDB) {
                        identity { Id<UserSession>().id }
@@ -131,59 +116,15 @@ fun Application.factbooks() {
                
                // Factbooks and assets
                
-               static("/static") {
-                       get("{static-content...}") {
-                               val staticContentPath = call.parameters.getAll("static-content")?.joinToString("/") ?: return@get
-                               val contentPath = "/static/$staticContentPath"
-                               
-                               ResourceLoader.getResource("$contentPath.sha256")?.reader()?.readText()?.let { sha256Hash ->
-                                       call.attributes.put(ResourceLoader.SHA256AttributeKey, sha256Hash)
-                               }
-                               
-                               val brContentPath = "$contentPath.br"
-                               val gzContentPath = "$contentPath.gz"
-                               
-                               val contentType = ContentType.fromFileExtension(contentPath.substringAfterLast('.')).firstOrNull()
-                               
-                               val acceptedEncodings = call.request.acceptEncodingItems().map { it.value }.toSet()
-                               
-                               if (CompressedFileType.BROTLI.encoding in acceptedEncodings) {
-                                       val brContent = ResourceLoader.getResource(brContentPath)
-                                       if (brContent != null) {
-                                               call.attributes.put(SuppressionAttribute, true)
-                                               
-                                               call.response.header(HttpHeaders.ContentEncoding, CompressedFileType.BROTLI.encoding)
-                                               
-                                               call.respondBytes(brContent.readBytes(), contentType)
-                                               
-                                               return@get
-                                       }
-                               }
-                               
-                               if (CompressedFileType.GZIP.encoding in acceptedEncodings) {
-                                       val gzContent = ResourceLoader.getResource(gzContentPath)
-                                       if (gzContent != null) {
-                                               call.attributes.put(SuppressionAttribute, true)
-                                               
-                                               call.response.header(HttpHeaders.ContentEncoding, CompressedFileType.GZIP.encoding)
-                                               
-                                               call.respondBytes(gzContent.readBytes(), contentType)
-                                               
-                                               return@get
-                                       }
-                               }
-                               
-                               ResourceLoader.getResource(contentPath)?.let { call.respondBytes(it.readBytes(), contentType) }
-                       }
+               staticResources("/static", "static", index = null) {
+                       preCompressed(CompressedFileType.BROTLI, CompressedFileType.GZIP)
                }
                
                get("/lore/{path...}") {
                        call.respondHtml(HttpStatusCode.OK, call.loreArticlePage())
                }
                
-               static("/assets") {
-                       files(File(Configuration.CurrentConfiguration.assetDir))
-               }
+               staticFiles("/assets", File(Configuration.CurrentConfiguration.assetDir), index = null)
                
                // Client settings
                
index 42e044ecfa4fc9e8f95479eb8099d4cd61e6a0d5..b069c6e73a1ff6efa6e4793f613e6e71b3303d7b 100644 (file)
@@ -88,7 +88,8 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id<NationData
                        comment.lastEdit?.let { lastEdit ->
                                p {
                                        style = "font-size:0.8em"
-                                       +"Edited ${comment.numEdits} times, last edited at "
+                                       val nounSuffix = if (comment.numEdits != 1) "s" else ""
+                                       +"Edited ${comment.numEdits} time$nounSuffix, last edited at "
                                        dateTime(lastEdit)
                                }
                        }
index a96343eaddf39f7b7c4da5e0f9f9cb30ed60ef9e..fb1726ff3db7dd9db2b6eec75275f83e9aeeb885 100644 (file)
@@ -36,9 +36,8 @@
                // Preview themes
                const themeChoices = document.getElementsByName("theme");
                for (const themeChoice of themeChoices) {
-                       const theme = themeChoice.value;
-                       themeChoice.addEventListener("click", () => {
-                               document.documentElement.setAttribute("data-theme", theme);
+                       themeChoice.addEventListener("click", e => {
+                               document.documentElement.setAttribute("data-theme", e.currentTarget.value);
                        });
                }
        });
 
        window.addEventListener("load", function () {
                // Image previewing
-               const thumbView = document.getElementById("thumb-view");
-               const thumbViewImg = thumbView.getElementsByTagName("img")[0];
-               thumbView.addEventListener("click", e => {
+               document.getElementById("thumb-view").addEventListener("click", e => {
                        e.preventDefault();
 
-                       thumbView.classList.remove("visible");
-                       thumbViewImg.src = "";
+                       e.currentTarget.classList.remove("visible");
+                       e.currentTarget.getElementsByTagName("img")[0].src = "";
                });
 
                const thumbs = document.querySelectorAll("a.thumb");
@@ -82,6 +79,8 @@
                        thumb.onclick = e => {
                                e.preventDefault();
 
+                               const thumbView = document.getElementById("thumb-view");
+                               const thumbViewImg = thumbView.getElementsByTagName("img")[0];
                                thumbViewImg.src = e.currentTarget.getAttribute("href");
                                thumbView.classList.add("visible");
                        };
 
                const canvases = document.getElementsByTagName("canvas");
                for (const canvas of canvases) {
-                       const modelName = canvas.getAttribute("data-model");
-                       if (modelName == null || modelName === "") continue;
+                       const canvasModelName = canvas.getAttribute("data-model");
+                       if (canvasModelName == null || canvasModelName === "") continue;
 
-                       (async () => {
+                       (async (modelName) => {
                                const modelAsync = loadObj(modelName);
 
                                const camera = new THREE.PerspectiveCamera(69, 1, 0.01, 1000.0);
                                light.position.set(0, 0, 0);
 
                                render();
-                       })().catch(reason => {
-                               console.error("Error rendering model " + modelName, reason);
+                       })(canvasModelName).catch(reason => {
+                               console.error("Error rendering model " + canvasModelName, reason);
                        });
                }
        });
 
                                let form = document.createElement("form");
                                form.style.display = "none";
-                               form.action = anchor.href;
-                               form.method = method;
+                               form.action = e.currentTarget.href;
+                               form.method = e.currentTarget.getAttribute("data-method");
 
-                               const csrfToken = anchor.getAttribute("data-csrf-token");
+                               const csrfToken = e.currentTarget.getAttribute("data-csrf-token");
                                if (csrfToken != null) {
                                        let csrfInput = document.createElement("input");
                                        csrfInput.name = "csrf-token";
index 3b77eb7c34fde5cfcd5f4249a539a955f77a97db..126055384dab78ecdd6789eb68f90d69237c8710 100644 (file)
@@ -626,7 +626,7 @@ div.list {
        display: flex;
        flex-wrap: nowrap;
        align-items: stretch;
-       justify-content: center;
+       justify-content: start;
        flex-direction: column;
 
        margin: 0;