Update brett

This commit is contained in:
Lars Westermann 2019-06-10 17:56:21 +02:00
parent d52ada1726
commit 0026643a0a
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
11 changed files with 197 additions and 71 deletions

View file

@ -6,6 +6,7 @@ debug = false
[schedule] [schedule]
reference = "2019-06-10" reference = "2019-06-10"
offset = 7200000
[general] [general]
wiki_url = "https://wiki.kif.rocks/w/index.php?title=KIF470:Arbeitskreise&action=raw" wiki_url = "https://wiki.kif.rocks/w/index.php?title=KIF470:Arbeitskreise&action=raw"

View file

@ -4,37 +4,52 @@ import com.soywiz.klock.DateFormat
import com.soywiz.klock.KlockLocale import com.soywiz.klock.KlockLocale
import com.soywiz.klock.format import com.soywiz.klock.format
import com.soywiz.klock.locale.german import com.soywiz.klock.locale.german
import com.soywiz.klock.min
fun formatDateTime(unix: Long) = fun formatDateTime(unix: Long) =
DateFormat("EEEE, d. MMMM y HH:mm") DateFormat("EEEE, d. MMMM y HH:mm")
.withLocale(KlockLocale.german) .withLocale(KlockLocale.german)
.format(unix) .format(unix)
fun formatTimeDiff(diff: Long): String { fun formatTimeDiff(time: Long, now: Long): String {
var time = diff / 1000 var dt = (time - now) / 1000
val seconds = time % 60 val seconds = dt % 60
time /= 60 dt /= 60
val minutes = time % 60 val minutes = dt % 60
time /= 60 dt /= 60
val hours = time % 24 val hours = dt % 24
time /= 24 dt /= 24
val days = time val days = dt
return when { return when {
days > 1L -> {
"in $days Tagen"
}
days > 0L -> { days > 0L -> {
if (days == 1L) "1 Tag" else "$days Tagen" "morgen"
}
hours > 1L -> {
val nowHour = DateFormat("HH")
.withLocale(KlockLocale.german)
.format(now).toInt() ?: 0
val ht = DateFormat("HH:mm")
.withLocale(KlockLocale.german)
.format(time)
if ((ht.substringBefore(":").toIntOrNull() ?: 0) < nowHour ) {
"morgen"
} else "um $ht"
} }
hours > 0L -> { hours > 0L -> {
hours.toString().padStart(2, '0')+":"+ (minutes + if (seconds > 0) 1 else 0).toString().padStart(2,'0') "in " +hours.toString().padStart(2, '0') + ":" + (minutes + if (seconds > 0) 1 else 0).toString().padStart(2, '0')
} }
minutes > 0L -> { minutes > 0L -> {
"00:"+ (minutes+ if (seconds > 0) 1 else 0).toString().padStart(2,'0') "in 00:" + (minutes + if (seconds > 0) 1 else 0).toString().padStart(2, '0')
} }
seconds > 0L -> { seconds > 0L -> {
"> 1 Minute" "in > 1 Minute"
} }
else -> "vor $minutes Minuten" else -> "---"
} }
} }

View file

@ -26,7 +26,7 @@ object ScheduleRepository : Repository<Schedule> {
} }
suspend fun getUpcoming(count: Int = 8): List<Schedule> { suspend fun getUpcoming(count: Int = 8): List<Schedule> {
val json = repositoryGet("$prefix/api/schedule/upcoming?count=$count") ?: return emptyList() val json = repositoryGet("$prefix/api/schedules/upcoming?count=$count") ?: return emptyList()
return parser.parse(json, Schedule.serializer().list) return parser.parse(json, Schedule.serializer().list)
} }

View file

@ -3,8 +3,9 @@ package de.kif.frontend.views.board
import com.soywiz.klock.DateFormat import com.soywiz.klock.DateFormat
import com.soywiz.klock.DateTimeTz import com.soywiz.klock.DateTimeTz
import com.soywiz.klock.KlockLocale import com.soywiz.klock.KlockLocale
import com.soywiz.klock.format
import com.soywiz.klock.locale.german import com.soywiz.klock.locale.german
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.kif.frontend.views.overview.getByClassOrCreate import de.kif.frontend.views.overview.getByClassOrCreate
import de.westermann.kwebview.interval import de.westermann.kwebview.interval
import de.westermann.kwebview.iterator import de.westermann.kwebview.iterator
@ -12,6 +13,7 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.browser.document import kotlin.browser.document
import kotlin.dom.clear
import kotlin.js.Date import kotlin.js.Date
fun initBoard() { fun initBoard() {
@ -27,9 +29,30 @@ fun initBoard() {
val boardRunning = document.getElementsByClassName("board-running")[0] as HTMLElement val boardRunning = document.getElementsByClassName("board-running")[0] as HTMLElement
val scheduleList = mutableListOf<BoardSchedule>() val scheduleList = mutableListOf<BoardSchedule>()
val runningReferenceTime = boardRunning.dataset["reference"]?.toLongOrNull() ?: 0L
fun update() {
launch {
scheduleList.clear()
val list = ScheduleRepository.getUpcoming()
boardRunning.clear()
val now = Date.now().toLong() + diff
for (s in list) {
val v = BoardSchedule.create(s, runningReferenceTime, now)
scheduleList += v
boardRunning.appendChild(v.html)
}
}
}
for (bs in boardRunning.getElementsByClassName("board-schedule").iterator()) { for (bs in boardRunning.getElementsByClassName("board-schedule").iterator()) {
scheduleList += BoardSchedule(bs) scheduleList += BoardSchedule(bs).also {
it.onRemove {
update()
}
}
} }
interval(1000) { interval(1000) {
@ -48,4 +71,14 @@ fun initBoard() {
scheduleList.forEach { it.updateTime(now) } scheduleList.forEach { it.updateTime(now) }
} }
ScheduleRepository.onCreate {
update()
}
ScheduleRepository.onUpdate {
update()
}
ScheduleRepository.onDelete {
update()
}
} }

View file

@ -1,36 +1,67 @@
package de.kif.frontend.views.board package de.kif.frontend.views.board
import de.kif.common.formatDateTime
import de.kif.common.formatTimeDiff import de.kif.common.formatTimeDiff
import de.kif.common.model.Schedule import de.kif.common.model.Schedule
import de.kif.frontend.views.overview.getByClassOrCreate import de.kif.frontend.views.overview.getByClassOrCreate
import de.westermann.kobserve.event.EventHandler
import de.westermann.kwebview.View import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.js.Date
class BoardSchedule( class BoardSchedule(
view: HTMLElement view: HTMLElement,
startTime: Long = 0L,
endTime: Long = 0L
) : View(view) { ) : View(view) {
private val colorView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-color") val colorViewContainer = view.getByClassOrCreate<HTMLDivElement>("board-schedule-color")
private val timeView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-time") val colorView = colorViewContainer.getByClassOrCreate<HTMLSpanElement>("bsc")
private val nameView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-name") val timeView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-time")
private val roomView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-room") val nameView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-name")
val roomView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-room")
private val schedule: Long = view.dataset["id"]?.toLongOrNull() ?: 0L
private val startTime: Long = timeView.dataset["startTime"]?.toLongOrNull() ?: 0L val clockViewContainer = view.getByClassOrCreate<HTMLDivElement>("board-schedule-clock")
private val endTime: Long = timeView.dataset["endTime"]?.toLongOrNull() ?: 0L val clockView = clockViewContainer.getByClassOrCreate<HTMLElement>("material-icons", "i").also {
it.textContent = "alarm"
}
private val startTime: Long = timeView.dataset["startTime"]?.toLongOrNull() ?: startTime
private val endTime: Long = timeView.dataset["endTime"]?.toLongOrNull() ?: endTime
val onRemove = EventHandler<Unit>()
fun updateTime(now: Long) { fun updateTime(now: Long) {
timeView.textContent = if (startTime >= now) { timeView.textContent = when {
"Start in ${formatTimeDiff(startTime - now)}" startTime >= now -> "Start ${formatTimeDiff(startTime, now)}"
} else { endTime >= now -> "Ende ${formatTimeDiff(endTime, now)}"
"Ende in ${formatTimeDiff(endTime - now)}" else -> {
onRemove.emit(Unit)
"---"
} }
} }
init { classList["board-schedule-running"] = now in startTime..endTime
}
companion object {
fun create(schedule: Schedule, referenceTime: Long, now: Long): BoardSchedule {
val startTime = ((schedule.getAbsoluteStartTime() * 60 * 1000) + referenceTime)
val endTime = ((schedule.getAbsoluteEndTime() * 60 * 1000) + referenceTime)
val entry = BoardSchedule(createHtmlView(), startTime, endTime)
if (schedule.workGroup.track?.color != null) {
entry.colorView.style.backgroundColor = schedule.workGroup.track.color.toString()
}
entry.nameView.textContent = schedule.workGroup.name
entry.roomView.textContent = schedule.room.name
entry.updateTime(now)
return entry
}
} }
} }

View file

@ -22,6 +22,17 @@
&:first-child { &:first-child {
width: 70%; width: 70%;
float: left; float: left;
overflow: visible;
&:after {
content: '';
position: absolute;
top: -1rem;
bottom: 0;
left: 100%;
margin-left: 1rem;
border-right: solid 1px var(--table-border-color);
}
} }
&:last-child { &:last-child {
@ -38,6 +49,16 @@
& > div { & > div {
height: 100%; height: 100%;
} }
&:after {
content: '';
position: absolute;
left: 0;
width: 100%;
top: 100%;
margin-top: 0.5rem;
border-bottom: solid 1px var(--table-border-color);
}
} }
.board-content { .board-content {
@ -71,10 +92,23 @@
border-bottom: solid 1px var(--table-border-color); border-bottom: solid 1px var(--table-border-color);
width: calc(50% - 1rem); width: calc(50% - 1rem);
margin-left: 1rem; margin-left: 1rem;
position: relative;
&:nth-last-child(1), &:nth-last-child(2) { &:nth-last-child(1), &:nth-last-child(2) {
border-bottom: none; border-bottom: none;
} }
.board-schedule-clock {
position: absolute;
left: 1rem;
top: 0;
display: none;
color: var(--primary-color);
}
&.board-schedule-running .board-schedule-clock {
display: block;
}
} }
.board-schedule-color { .board-schedule-color {

View file

@ -504,7 +504,7 @@
content: ''; content: '';
display: block; display: block;
position: absolute; position: absolute;
top: 3rem; top: 0.1rem;
bottom: 0; bottom: 0;
width: 1px; width: 1px;
border-right: solid 1px var(--primary-color); border-right: solid 1px var(--primary-color);
@ -514,7 +514,7 @@
content: ''; content: '';
display: block; display: block;
position: absolute; position: absolute;
top: 3rem; top: 0.1rem;
transform: scale(0.5, 1) rotate(45deg); transform: scale(0.5, 1) rotate(45deg);
transform-origin: right; transform-origin: right;
border-bottom: solid 0.4rem var(--primary-color); border-bottom: solid 0.4rem var(--primary-color);

View file

@ -60,6 +60,7 @@ object Configuration {
private object ScheduleSpec : ConfigSpec("schedule") { private object ScheduleSpec : ConfigSpec("schedule") {
val reference by required<String>() val reference by required<String>()
val offset by required<Long>()
} }
object Schedule { object Schedule {
@ -69,6 +70,8 @@ object Configuration {
sdf.timeZone = TimeZone.getTimeZone(ZoneId.of("UTC")) sdf.timeZone = TimeZone.getTimeZone(ZoneId.of("UTC"))
sdf.parse(reference) sdf.parse(reference)
} }
val offset by c(ScheduleSpec.offset)
} }
private object SecuritySpec : ConfigSpec("security") { private object SecuritySpec : ConfigSpec("security") {

View file

@ -10,10 +10,7 @@ import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import kotlinx.css.CSSBuilder import kotlinx.css.CSSBuilder
import kotlinx.css.Color import kotlinx.css.Color
import kotlinx.html.div import kotlinx.html.*
import kotlinx.html.img
import kotlinx.html.span
import kotlinx.html.unsafe
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -25,8 +22,10 @@ data class BoardSchedule(
) )
suspend fun getUpcoming(limit: Int = 8): List<Schedule> { suspend fun getUpcoming(limit: Int = 8): List<Schedule> {
val now = (Date().time / (1000 * 60)).toInt() - (Configuration.Schedule.referenceDate.time / (1000 * 60)) val now =
return ScheduleRepository.all().asSequence() ((Date().time + Configuration.Schedule.offset) / (1000 * 60)).toInt() -
(Configuration.Schedule.referenceDate.time / (1000 * 60))
return ScheduleRepository.all()
.map { .map {
BoardSchedule( BoardSchedule(
it, it,
@ -35,10 +34,11 @@ suspend fun getUpcoming(limit: Int = 8): List<Schedule> {
) )
} }
.filter { it.endTime > now } .filter { it.endTime > now }
.sortedBy { it.startTime } .partition { it.startTime < now }.let { (running, upcoming) ->
running.sortedBy { it.endTime } + upcoming.sortedBy { it.startTime }
}
.take(limit) .take(limit)
.map { it.schedule } .map { it.schedule }
.toList()
} }
fun Route.board() { fun Route.board() {
@ -47,6 +47,7 @@ fun Route.board() {
val now = Date() val now = Date()
val referenceTime = Configuration.Schedule.referenceDate.time val referenceTime = Configuration.Schedule.referenceDate.time
val timeOffset = Configuration.Schedule.offset
val refDate = Configuration.Schedule.referenceDate val refDate = Configuration.Schedule.referenceDate
@ -83,17 +84,28 @@ fun Route.board() {
min = (min / 60 - 1) * 60 min = (min / 60 - 1) * 60
max = (max / 60 + 2) * 60 max = (max / 60 + 2) * 60
val nowLocale = now.time + timeOffset
respondMain(true, true) { theme -> respondMain(true, true) { theme ->
content { content {
div("board") { div("board") {
div("board-header") { div("board-header") {
div("board-running") { div("board-running") {
attributes["data-reference"] = referenceTime.toString()
for (schedule in scheduleList) { for (schedule in scheduleList) {
div("board-schedule") { val startTime = ((schedule.getAbsoluteStartTime() * 60 * 1000) + referenceTime)
val endTime = ((schedule.getAbsoluteEndTime() * 60 * 1000) + referenceTime)
var classes = "board-schedule"
if (nowLocale in startTime..endTime) {
classes += " board-schedule-running"
}
div(classes) {
attributes["data-id"] = schedule.id.toString() attributes["data-id"] = schedule.id.toString()
div("board-schedule-color") { div("board-schedule-color") {
span { span("bsc") {
attributes["style"] = CSSBuilder().apply { attributes["style"] = CSSBuilder().apply {
val c = schedule.workGroup.track?.color val c = schedule.workGroup.track?.color
if (c != null) { if (c != null) {
@ -104,16 +116,14 @@ fun Route.board() {
} }
div("board-schedule-time") { div("board-schedule-time") {
val startTime = ((schedule.getAbsoluteStartTime() * 60 * 1000) + referenceTime)
val endTime = ((schedule.getAbsoluteEndTime() * 60 * 1000) + referenceTime)
attributes["data-start-time"] = startTime.toString() attributes["data-start-time"] = startTime.toString()
attributes["data-end-time"] = endTime.toString() attributes["data-end-time"] = endTime.toString()
if (startTime >= now.time) { if (startTime >= nowLocale) {
+"Start in ${formatTimeDiff(startTime - now.time)}" +"Start ${formatTimeDiff(startTime, nowLocale)}"
} else { } else {
+"Ende in ${formatTimeDiff(endTime - now.time)}" +"Ende ${formatTimeDiff(endTime, nowLocale)}"
} }
} }
@ -124,14 +134,17 @@ fun Route.board() {
div("board-schedule-room") { div("board-schedule-room") {
+schedule.room.name +schedule.room.name
} }
div("board-schedule-clock") {
i("material-icons") { +"alarm" }
}
} }
} }
} }
div("board-logo") { div("board-logo") {
img("KIF 47.0", "/static/images/logo.svg") img("KIF 47.0", "/static/images/logo.svg")
div("board-header-date") { div("board-header-date") {
attributes["data-now"] = attributes["data-now"] = (now.time - timeOffset).toString()
(now.time - 2 * 60 * 60 * 1000).toString()
} }
} }
} }

View file

@ -1,19 +1,13 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.Configuration import de.kif.backend.*
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.repository.PostRepository
import de.kif.backend.util.markdownToHtml 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.backend.view.respondMain
import de.kif.common.formatDateTime import de.kif.common.formatDateTime
import de.kif.common.model.Permission import de.kif.common.model.Permission
import de.kif.common.model.Post import de.kif.common.model.Post
import io.ktor.application.call import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart import io.ktor.http.content.forEachPart
@ -26,7 +20,6 @@ import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
import kotlinx.html.* import kotlinx.html.*
import java.io.File import java.io.File
import de.kif.backend.prefix
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") { fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
var classes = "post" var classes = "post"
@ -80,13 +73,6 @@ fun Route.overview() {
respondMain { theme -> respondMain { theme ->
content { content {
div("overview") {
div("overview-main") {
for (post in postList) {
createPost(post, editable, "overview-post")
}
}
div("overview-side") {
if (editable) { if (editable) {
div("overview-new") { div("overview-new") {
a("post/new", classes = "form-btn") { a("post/new", classes = "form-btn") {
@ -94,9 +80,17 @@ fun Route.overview() {
} }
} }
} }
div("overview") {
div("overview-main") {
for (post in postList) {
createPost(post, editable, "overview-post")
}
}
div("overview-side") {
div("overview-twitter") { div("overview-twitter") {
unsafe { unsafe {
raw(""" raw(
"""
<a <a
class="twitter-timeline" class="twitter-timeline"
href="${Configuration.Twitter.timeline}" href="${Configuration.Twitter.timeline}"
@ -108,7 +102,8 @@ fun Route.overview() {
data-dnt="true" data-dnt="true"
>Twitter wall</a> >Twitter wall</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
""".trimIndent()) """.trimIndent()
)
} }
} }
} }

View file

@ -12,6 +12,7 @@ database = "data/portal.db"
[schedule] [schedule]
reference = "1970-01-01" reference = "1970-01-01"
offset = 0
[security] [security]
session_name = "SESSION" session_name = "SESSION"