552 lines
23 KiB
Kotlin
552 lines
23 KiB
Kotlin
package de.kif.backend.route
|
|
|
|
import de.kif.backend.Configuration
|
|
import de.kif.backend.Resources
|
|
import de.kif.backend.authenticateOrRedirect
|
|
import de.kif.backend.isAuthenticated
|
|
import de.kif.backend.repository.PostRepository
|
|
import de.kif.backend.util.markdownToHtml
|
|
import de.kif.backend.view.MainTemplate
|
|
import de.kif.backend.view.MenuTemplate
|
|
import de.kif.backend.view.respondMain
|
|
import de.kif.common.formatDateTime
|
|
import de.kif.common.model.Permission
|
|
import de.kif.common.model.Post
|
|
import io.ktor.application.call
|
|
import io.ktor.html.respondHtmlTemplate
|
|
import io.ktor.http.HttpStatusCode
|
|
import io.ktor.http.content.PartData
|
|
import io.ktor.http.content.forEachPart
|
|
import io.ktor.http.content.streamProvider
|
|
import io.ktor.request.receiveMultipart
|
|
import io.ktor.response.respond
|
|
import io.ktor.response.respondRedirect
|
|
import io.ktor.routing.Route
|
|
import io.ktor.routing.get
|
|
import io.ktor.routing.post
|
|
import kotlinx.html.*
|
|
import java.io.File
|
|
|
|
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
|
|
var classes = "post"
|
|
if (additionalClasses.isNotBlank()) {
|
|
classes += " $additionalClasses"
|
|
}
|
|
if (post.image == null) {
|
|
classes += " post-no-image"
|
|
}
|
|
div(classes) {
|
|
attributes["data-id"] = post.id.toString()
|
|
attributes["data-pinned"] = post.pinned.toString()
|
|
|
|
a("/p/${post.url}", classes = "post-name") {
|
|
+post.name
|
|
}
|
|
if (editable) {
|
|
a("/post/${post.id}", classes = "post-edit") {
|
|
i("material-icons") { +"edit" }
|
|
}
|
|
}
|
|
|
|
div("post-column") {
|
|
div("post-column-left") {
|
|
figure(classes = "post-image") {
|
|
if (post.image != null) {
|
|
attributes["style"] = "background-image: url(\"/images/${post.image}\")"
|
|
}
|
|
}
|
|
}
|
|
div("post-column-right") {
|
|
div("post-content") {
|
|
unsafe {
|
|
raw(markdownToHtml(post.content))
|
|
}
|
|
}
|
|
div("post-footer") {
|
|
+formatDateTime(post.createdAt)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun Route.overview() {
|
|
get("") {
|
|
val user = isAuthenticated(Permission.POST)
|
|
val editable = user != null
|
|
|
|
val postList = PostRepository.all().asReversed()
|
|
|
|
respondMain { theme ->
|
|
content {
|
|
div("overview") {
|
|
div("overview-main") {
|
|
for (post in postList) {
|
|
createPost(post, editable, "overview-post")
|
|
}
|
|
}
|
|
div("overview-side") {
|
|
if (editable) {
|
|
div("overview-new") {
|
|
a("post/new", classes = "form-btn") {
|
|
+"New"
|
|
}
|
|
}
|
|
}
|
|
div("overview-twitter") {
|
|
unsafe {
|
|
raw("""
|
|
<a
|
|
class="twitter-timeline"
|
|
href="${Configuration.Twitter.timeline}"
|
|
data-chrome="transparent"
|
|
data-theme="${if (theme.dark) "dark" else "light"}"
|
|
data-link-color="${theme.primaryColor}"
|
|
data-cards="hidden"
|
|
data-lang="de"
|
|
data-dnt="true"
|
|
>Twitter wall</a>
|
|
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
|
""".trimIndent())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get("/p/{url}") {
|
|
val user = isAuthenticated(Permission.POST)
|
|
val editable = user != null
|
|
|
|
val post = PostRepository.getByUrl(call.parameters["url"] ?: "")
|
|
|
|
if (post == null) {
|
|
call.respond(HttpStatusCode.NotFound, "Not found")
|
|
return@get
|
|
}
|
|
|
|
respondMain {
|
|
content {
|
|
div("overview") {
|
|
createPost(post, editable)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get("/post/{id}") {
|
|
authenticateOrRedirect(Permission.POST) { user ->
|
|
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
|
val editPost = PostRepository.get(postId) ?: return@get
|
|
respondMain {
|
|
content {
|
|
h1 { +"Edit post" }
|
|
div("post-edit-container") {
|
|
div("post-edit-left") {
|
|
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
|
|
div("form-group") {
|
|
label {
|
|
htmlFor = "name"
|
|
+"Name"
|
|
}
|
|
input(
|
|
name = "name",
|
|
classes = "form-control"
|
|
) {
|
|
id = "name"
|
|
placeholder = "Name"
|
|
value = editPost.name
|
|
}
|
|
}
|
|
|
|
div("form-group") {
|
|
label {
|
|
htmlFor = "url"
|
|
+"Url"
|
|
}
|
|
input(
|
|
name = "url",
|
|
classes = "form-control"
|
|
) {
|
|
id = "url"
|
|
placeholder = "Url"
|
|
value = editPost.url
|
|
}
|
|
}
|
|
|
|
label {
|
|
htmlFor = "image"
|
|
+"Image"
|
|
}
|
|
div("post-edit-image-box") {
|
|
div {
|
|
figure(classes = "post-edit-image") {
|
|
if (editPost.image != null) {
|
|
attributes["style"] =
|
|
"background-image: url(\"/images/${editPost.image}\")"
|
|
}
|
|
}
|
|
}
|
|
div {
|
|
div("form-group") {
|
|
input(
|
|
name = "image",
|
|
classes = "form-btn",
|
|
type = InputType.file
|
|
) {
|
|
id = "image"
|
|
value = "Upload image"
|
|
accept =
|
|
Configuration.General
|
|
.allowedUploadExtensionSet
|
|
.joinToString(",") {
|
|
".$it"
|
|
}
|
|
}
|
|
}
|
|
|
|
div("form-group form-switch") {
|
|
input(
|
|
name = "image-delete",
|
|
classes = "form-control",
|
|
type = InputType.checkBox
|
|
) {
|
|
id = "image-delete"
|
|
checked = false
|
|
}
|
|
label {
|
|
htmlFor = "image-delete"
|
|
+"Delete image"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
div("form-switch-group") {
|
|
div("form-group form-switch") {
|
|
input(
|
|
name = "pinned",
|
|
classes = "form-control",
|
|
type = InputType.checkBox
|
|
) {
|
|
id = "pinned"
|
|
checked = editPost.pinned
|
|
}
|
|
label {
|
|
htmlFor = "pinned"
|
|
+"Pinned"
|
|
}
|
|
}
|
|
div("form-group form-switch") {
|
|
input(
|
|
name = "hide-on-projector",
|
|
classes = "form-control",
|
|
type = InputType.checkBox
|
|
) {
|
|
id = "hide-on-projector"
|
|
checked = editPost.hideOnProjector
|
|
}
|
|
label {
|
|
htmlFor = "hide-on-projector"
|
|
+"Hide on projector"
|
|
}
|
|
}
|
|
}
|
|
|
|
div("form-group") {
|
|
label {
|
|
htmlFor = "content"
|
|
+"Content"
|
|
}
|
|
textArea(rows = "10", classes = "form-control") {
|
|
name = "content"
|
|
id = "content"
|
|
|
|
+editPost.content
|
|
}
|
|
}
|
|
|
|
div("form-group") {
|
|
a("/") {
|
|
button(classes = "form-btn") {
|
|
+"Cancel"
|
|
}
|
|
}
|
|
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
|
+"Save"
|
|
}
|
|
}
|
|
}
|
|
a("/post/${editPost.id}/delete") {
|
|
button(classes = "form-btn btn-danger") {
|
|
+"Delete"
|
|
}
|
|
}
|
|
}
|
|
div("post-edit-right post-content") {
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
post("/post/{id}") {
|
|
authenticateOrRedirect(Permission.POST) { user ->
|
|
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
|
|
|
|
var imageUploadName: String? = null
|
|
|
|
val params = mutableMapOf<String, String>()
|
|
call.receiveMultipart().forEachPart { part ->
|
|
val name = part.name ?: return@forEachPart
|
|
when (part) {
|
|
is PartData.FormItem -> {
|
|
params[name] = part.value
|
|
}
|
|
is PartData.FileItem -> {
|
|
val extension = File(part.originalFileName).extension
|
|
|
|
if (extension.toLowerCase() !in Configuration.General.allowedUploadExtensionSet) return@forEachPart
|
|
|
|
var uploadName = Post.generateUrl() + "." + extension
|
|
|
|
while (true) {
|
|
if (Resources.existsUpload(uploadName)) {
|
|
uploadName = Post.generateUrl() + "." + extension
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
Resources.createUpload(uploadName, part.streamProvider())
|
|
imageUploadName = uploadName
|
|
}
|
|
}
|
|
}
|
|
|
|
var post = PostRepository.get(postId) ?: return@post
|
|
|
|
params["name"]?.let { post = post.copy(name = it) }
|
|
params["url"]?.let { post = post.copy(url = it) }
|
|
params["content"]?.let { post = post.copy(content = it) }
|
|
params["pinned"]?.let { post = post.copy(pinned = it == "on") }
|
|
params["hide-on-projector"]?.let { post = post.copy(hideOnProjector = it == "on") }
|
|
|
|
if (params["image-delete"] == "on") {
|
|
val currentImage = post.image
|
|
if (currentImage != null) {
|
|
post = post.copy(image = null)
|
|
Resources.deleteUpload(currentImage)
|
|
}
|
|
|
|
val upload = imageUploadName
|
|
if (upload != null) {
|
|
Resources.deleteUpload(upload)
|
|
}
|
|
} else {
|
|
imageUploadName?.let {
|
|
val currentImage = post.image
|
|
if (currentImage != null) {
|
|
Resources.deleteUpload(currentImage)
|
|
}
|
|
|
|
post = post.copy(image = it)
|
|
}
|
|
}
|
|
|
|
PostRepository.update(post)
|
|
|
|
call.respondRedirect("/")
|
|
}
|
|
}
|
|
|
|
get("/post/new") {
|
|
authenticateOrRedirect(Permission.POST) { user ->
|
|
respondMain {
|
|
content {
|
|
h1 { +"Create post" }
|
|
div("post-edit-container") {
|
|
div("post-edit-left") {
|
|
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
|
|
div("form-group") {
|
|
label {
|
|
htmlFor = "name"
|
|
+"Name"
|
|
}
|
|
input(
|
|
name = "name",
|
|
classes = "form-control"
|
|
) {
|
|
id = "name"
|
|
placeholder = "Name"
|
|
value = ""
|
|
}
|
|
}
|
|
div("form-group") {
|
|
label {
|
|
htmlFor = "url"
|
|
+"Url"
|
|
}
|
|
input(
|
|
name = "url",
|
|
classes = "form-control"
|
|
) {
|
|
id = "url"
|
|
placeholder = "Url"
|
|
value = Post.generateUrl()
|
|
}
|
|
}
|
|
|
|
label {
|
|
htmlFor = "image"
|
|
+"Image"
|
|
}
|
|
div("post-edit-image-box") {
|
|
div {
|
|
figure(classes = "post-edit-image") {}
|
|
}
|
|
div {
|
|
div("form-group") {
|
|
input(
|
|
name = "image",
|
|
classes = "form-btn",
|
|
type = InputType.file
|
|
) {
|
|
id = "image"
|
|
value = "Upload image"
|
|
accept =
|
|
Configuration.General
|
|
.allowedUploadExtensionSet
|
|
.joinToString(",") {
|
|
".$it"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
div("form-switch-group") {
|
|
div("form-group form-switch") {
|
|
input(
|
|
name = "pinned",
|
|
classes = "form-control",
|
|
type = InputType.checkBox
|
|
) {
|
|
id = "pinned"
|
|
checked = false
|
|
}
|
|
label {
|
|
htmlFor = "pinned"
|
|
+"Pinned"
|
|
}
|
|
}
|
|
div("form-group form-switch") {
|
|
input(
|
|
name = "hide-on-projector",
|
|
classes = "form-control",
|
|
type = InputType.checkBox
|
|
) {
|
|
id = "hide-on-projector"
|
|
checked = false
|
|
}
|
|
label {
|
|
htmlFor = "hide-on-projector"
|
|
+"Hide on projector"
|
|
}
|
|
}
|
|
}
|
|
|
|
div("form-group") {
|
|
label {
|
|
htmlFor = "content"
|
|
+"Content"
|
|
}
|
|
textArea(rows = "10", classes = "form-control") {
|
|
name = "content"
|
|
id = "content"
|
|
|
|
+""
|
|
}
|
|
}
|
|
|
|
div("form-group") {
|
|
a("/") {
|
|
button(classes = "form-btn") {
|
|
+"Cancel"
|
|
}
|
|
}
|
|
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
|
+"Create"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
div("post-edit-right post-content") {
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
post("/post/new") {
|
|
authenticateOrRedirect(Permission.POST) { user ->
|
|
var imageUploadName: String? = null
|
|
|
|
val params = mutableMapOf<String, String>()
|
|
call.receiveMultipart().forEachPart { part ->
|
|
val name = part.name ?: return@forEachPart
|
|
when (part) {
|
|
is PartData.FormItem -> {
|
|
params[name] = part.value
|
|
}
|
|
is PartData.FileItem -> {
|
|
val extension = File(part.originalFileName).extension
|
|
|
|
if (extension.toLowerCase() !in Configuration.General.allowedUploadExtensionSet) return@forEachPart
|
|
|
|
var uploadName = Post.generateUrl() + "." + extension
|
|
|
|
while (true) {
|
|
if (Resources.existsUpload(uploadName)) {
|
|
uploadName = Post.generateUrl() + "." + extension
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
Resources.createUpload(uploadName, part.streamProvider())
|
|
imageUploadName = uploadName
|
|
}
|
|
}
|
|
}
|
|
|
|
val name = params["name"] ?: return@post
|
|
val content = params["content"] ?: return@post
|
|
val url = params["url"] ?: return@post
|
|
val pinned = params["pinned"] == "on"
|
|
val hideOnProjector = params["hide-on-projector"] == "on"
|
|
|
|
val post = Post(null, name, content, url, imageUploadName, pinned, hideOnProjector)
|
|
|
|
PostRepository.create(post)
|
|
|
|
call.respondRedirect("/")
|
|
}
|
|
}
|
|
|
|
get("/post/{id}/delete") {
|
|
authenticateOrRedirect(Permission.POST) { user ->
|
|
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
|
|
|
PostRepository.delete(postId)
|
|
|
|
call.respondRedirect("/")
|
|
}
|
|
}
|
|
}
|