Compare commits

..

No commits in common. "2b290930867138ead7468294f8c43e2a27c8a962" and "dce2567160555ee7a37f5011216d80683930b239" have entirely different histories.

15 changed files with 251 additions and 255 deletions

View file

@ -99,6 +99,10 @@ kotlin {
implementation "com.soywiz:klock-jvm:$klockVersion"
implementation "com.soywiz:klock-locale-jvm:$klockVersion"
//implementation 'com.twitter:hbc-core:2.2.0'
//implementation 'com.twitter:hbc-twitter4j:2.2.0'
api 'org.twitter4j:twitter4j-stream:4.0.1'
implementation 'com.github.uchuhimo:konf:master-SNAPSHOT'
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
api 'io.github.microutils:kotlin-logging:1.6.23'

View file

@ -1,6 +1,5 @@
$background-primary-color: #fff;
$background-secondary-color: #f5f5f5;
$background-card-color: #fff;
$background-secondary-color: #fcfcfc;
$text-primary-color: #333;
$text-secondary-color: rgba($text-primary-color, 0.5);
@ -16,7 +15,7 @@ $input-border-color: #888;
$table-border-color: rgba($text-primary-color, 0.1);
$table-header-color: rgba($text-primary-color, 0.06);
$shadow-color: rgba(#000, 0.3);
$shadow-color: rgba($text-primary-color, 0.8);
$icon-color-focused: rgba($text-primary-color, 0.87);
$icon-color: rgba($text-primary-color, 0.54);
@ -31,7 +30,6 @@ $lever-enabled-color: $primary-color;
:root {
--background-primary-color: $background-primary-color;
--background-secondary-color: $background-secondary-color;
--background-card-color: $background-card-color;
--text-primary-color: $text-primary-color;
--text-secondary-color: $text-secondary-color;

View file

@ -1 +0,0 @@

View file

@ -1,124 +1,73 @@
@import "../config";
.board-header {
line-height: 3rem;
flex-grow: 1;
font-family: "Bungee", sans-serif;
font-weight: normal;
font-size: 1.1rem;
padding-left: 0.3rem;
}
line-height: 6rem;
display: flex;
padding: 0 2rem;
.board-card {
background-color: var(--background-card-color);
box-shadow: 0 0.1rem 0.2rem var(--shadow-color);
border-radius: $border-radius;
overflow: hidden;
& > div {
flex-grow: 1;
font-family: "Bungee", sans-serif;
font-weight: normal;
font-size: 1.1rem;
text-align: center;
&:first-child {
text-align: left;
}
&:last-child {
text-align: right;
}
}
}
.board {
padding: 1rem 0.4rem 2rem;
padding: 1rem 1rem 2rem;
display: flex;
overflow: hidden;
height: calc(100vh - 0.5rem);
height: calc(100vh - 6rem);
& > div {
flex-grow: 1;
flex-basis: 0;
padding: 0 0.4rem;
&:nth-child(1) {
flex-grow: 4;
padding-left: 0.15rem;
padding-right: 0.15rem;
}
&:nth-child(2) {
flex-grow: 3;
}
&:nth-child(3) {
flex-grow: 3;
}
}
}
.board-twitter {
.board-card {
height: calc(100% - 1.3rem);
& > * {
margin-top: -1px !important;
}
}
}
.board-post {
margin-bottom: 0.5rem;
.post-name {
top: 0.5rem;
}
padding-top: 2.5rem;
padding-bottom: 0.5rem;
}
.board-schedule-box {
display: flex;
flex-wrap: wrap;
}
.board-schedule {
width: 100%;
position: relative;
flex-grow: 1;
flex-basis: 0;
min-width: 15rem;
padding: 0.6rem;
margin: 0 0.25rem 0.5rem;
padding: 1rem 1rem;
&:not(:last-child) {
border-bottom: solid 1px var(--table-border-color)
}
}
.board-schedule-bottom {
.board-schedule-room {
display: block;
padding-left: 1rem;
padding-right: 0.5rem;
line-height: 1rem;
clear: both;
line-height: 2rem;
}
& > span {
&:first-child {
float: left;
}
&:last-child {
float: right;
}
}
.board-schedule-time {
position: absolute;
top: 1rem;
right: 1rem;
line-height: 2rem;
}
.board-schedule-color {
background-color: var(--primary-color);
position: absolute;
top: 1.25rem;
left: 0.6rem;
width: 0.8rem;
height: 0.8rem;
border-radius: 100%;
top: 3.25rem;
left: 1rem;
width: 0.5rem;
height: 1.5rem;
}
.board-schedule-name {
display: block;
padding-left: 1rem;
line-height: 1.3rem;
font-family: 'Montserrat', sans-serif;
font-weight: 600;
padding-top: 0.35rem;
padding-bottom: 0.35rem;
line-height: 2rem;
}

View file

@ -12,10 +12,12 @@
}
.overview-side {
min-width: 30%;
min-width: 20%;
}
.overview-twitter {
height: 20rem;
background-color: #b3e6f9;
}
.post {
@ -32,8 +34,6 @@
color: var(--primary-color);
line-height: 2rem;
padding: 0 1rem;
font-weight: bold;
font-family: 'Montserrat', sans-serif;
&:empty::before {
display: block;

View file

@ -2,12 +2,11 @@
$background-primary-color: #2d2d2d;
$background-secondary-color: #373737;
$background-card-color: rgba($text-primary-color, 0.06);
$text-primary-color: #fff;
$text-secondary-color: rgba($text-primary-color, 0.5);
$primary-color: #ef5350;
$primary-color: #dd213d;
$primary-text-color: #fff;
$error-color: #F00;
@ -18,7 +17,7 @@ $input-border-color: #888;
$table-border-color: rgba($text-primary-color, 0.1);
$table-header-color: rgba($text-primary-color, 0.06);
$shadow-color: rgba(#000, 0.3);
$shadow-color: rgba($text-primary-color, 0.8);
$icon-color-focused: rgba($text-primary-color, 1.0);
$icon-color: rgba($text-primary-color, 0.7);

View file

@ -2,7 +2,6 @@
$background-primary-color: #ffc3e1;
$background-secondary-color: #ffa5d2;
$background-card-color: rgba($text-primary-color, 0.06);
$text-primary-color: #333;
$text-secondary-color: rgba($text-primary-color, 0.5);
@ -18,7 +17,7 @@ $input-border-color: #888;
$table-border-color: rgba($text-primary-color, 0.1);
$table-header-color: rgba($text-primary-color, 0.06);
$shadow-color: rgba(#000, 0.3);
$shadow-color: rgba($text-primary-color, 0.8);
$icon-color-focused: rgba($text-primary-color, 0.87);
$icon-color: rgba($text-primary-color, 0.54);

View file

@ -14,8 +14,8 @@ body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 500;
font-family: 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 600;
width: 100%;
height: 100%;

View file

@ -91,11 +91,13 @@ object Configuration {
}
private object TwitterSpec : ConfigSpec("twitter") {
val timeline by required<String>("timeline")
val username by required<String>("username")
val password by required<String>("password")
}
object Twitter {
val timeline by c(TwitterSpec.timeline)
val username by c(TwitterSpec.username)
val password by c(TwitterSpec.password)
}
init {

View file

@ -0,0 +1,142 @@
package de.kif.backend
import twitter4j.*
import twitter4j.conf.ConfigurationBuilder
/*
fun twitter() {
val msgQueue = LinkedBlockingQueue<String>(100000)
val eventQueue = LinkedBlockingQueue<Event>(1000)
val hosts = HttpHosts(Constants.STREAM_HOST)
val filterEndpoint = StatusesFilterEndpoint()
filterEndpoint.trackTerms(listOf("kif"))
println(Configuration.Twitter.username)
println(Configuration.Twitter.password)
val authentication = BasicAuth(
Configuration.Twitter.username,
Configuration.Twitter.password
)
val builder = ClientBuilder()
.name("kif-portal")
.hosts(hosts)
.authentication(authentication)
.endpoint(filterEndpoint)
.processor(StringDelimitedProcessor(msgQueue))
.eventMessageQueue(eventQueue)
val client = builder.build()
val listener = object: StatusStreamHandler {
override fun onUnknownMessageType(msg: String?) {
println("onUnknownMessageType")
println(msg)
}
override fun onDisconnectMessage(message: DisconnectMessage?) {
println("onDisconnectMessage")
println(message?.disconnectReason)
}
override fun onStallWarningMessage(warning: StallWarningMessage?) {
println("onStallWarningMessage")
println(warning?.message)
}
override fun onTrackLimitationNotice(numberOfLimitedStatuses: Int) {
println("onTrackLimitationNotice")
println(numberOfLimitedStatuses)
}
override fun onStallWarning(warning: StallWarning?) {
println("onStallWarning")
println(warning?.message)
}
override fun onException(ex: Exception?) {
println("onException")
ex?.printStackTrace()
}
override fun onDeletionNotice(statusDeletionNotice: StatusDeletionNotice?) {
println("onDeletionNotice")
println(statusDeletionNotice?.statusId)
}
override fun onStatus(status: Status?) {
println("onStatus")
println(status?.text)
}
override fun onScrubGeo(userId: Long, upToStatusId: Long) {
println("onScrubGeo")
}
}
val t4jClient = Twitter4jStatusClient(
client,
msgQueue,
listOf(listener),
Executors.newFixedThreadPool(4)
)
t4jClient.connect()
t4jClient.process()
while (true) {
}
}
*/
fun twitter() {
val cb = ConfigurationBuilder()
cb.setDebugEnabled(true)
.setUser(Configuration.Twitter.username)
.setPassword(Configuration.Twitter.password)
val listener = object : StatusListener {
override fun onTrackLimitationNotice(numberOfLimitedStatuses: Int) {
println("onTrackLimitationNotice")
println(numberOfLimitedStatuses)
}
override fun onStallWarning(warning: StallWarning?) {
println("onStallWarning")
println(warning?.message)
}
override fun onException(ex: Exception?) {
println("onException")
ex?.printStackTrace()
}
override fun onDeletionNotice(statusDeletionNotice: StatusDeletionNotice?) {
println("onDeletionNotice")
println(statusDeletionNotice?.statusId)
}
override fun onStatus(status: Status?) {
println("onStatus")
println(status?.text)
}
override fun onScrubGeo(userId: Long, upToStatusId: Long) {
println("onScrubGeo")
}
}
val twitterStream = TwitterStreamFactory(cb.build()).instance
addTwitterStreamListener(twitterStream, listener)
twitterStream.sample()
}

View file

@ -2,15 +2,18 @@ package de.kif.backend.route
import de.kif.backend.authenticate
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.util.Backup
import de.kif.backend.repository.TrackRepository
import de.kif.backend.repository.WorkGroupRepository
import de.kif.backend.route.api.error
import de.kif.backend.util.Backup
import de.kif.backend.util.WikiImporter
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.respondMain
import de.kif.common.RepositoryType
import de.kif.common.model.Permission
import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
@ -32,6 +35,10 @@ private val logger = KotlinLogging.logger {}
fun Route.account() {
get("/account") {
authenticateOrRedirect { user ->
val tracks = TrackRepository.all()
val wikiSections = WikiImporter.loadSections()
respondMain {
content {
h1 { +"Account" }
@ -47,26 +54,6 @@ fun Route.account() {
}
}
a(href = "/account/backup", classes = "form-btn") {
+"Backup"
}
if (user.checkPermission(Permission.ADMIN)) {
a(href = "/account/import", classes = "form-btn") {
+"Import wiki"
}
}
}
}
}
}
get("/account/backup") {
authenticateOrRedirect { user ->
respondMain {
content {
h1 { +"Backup" }
div("account-backup") {
if (user.checkPermission(Permission.ROOM)) {
a("/account/backup/rooms.json", classes = "form-btn") {
@ -112,11 +99,9 @@ fun Route.account() {
}
if (user.checkPermission(Permission.ADMIN)) {
h1 { +"Restore" }
div("account-import") {
form(
action = "/account/restore",
action = "/account/import",
method = FormMethod.post,
encType = FormEncType.multipartFormData
) {
@ -161,25 +146,12 @@ fun Route.account() {
}
}
}
}
}
}
}
get("/account/import") {
authenticateOrRedirect(Permission.ADMIN) { user ->
val tracks = TrackRepository.all()
val wikiSections = WikiImporter.loadSections()
respondMain {
content {
h1 { +"Import wiki" }
if (user.checkPermission(Permission.ADMIN)) {
div("account-import-wiki") {
span { +"Import work group data from the kif wiki" }
form(action = "/account/import", method = FormMethod.post) {
form(action = "/account/import-wiki", method = FormMethod.post) {
for ((index, section) in wikiSections.withIndex()) {
div("form-group") {
label {
@ -305,7 +277,7 @@ fun Route.account() {
}
}
post("/account/restore") {
post("/account/import") {
authenticate(Permission.ADMIN) {
var reset = false
var import = ""
@ -336,7 +308,7 @@ fun Route.account() {
}
}
post("/account/import") {
post("/account/import-wiki") {
authenticate(Permission.ADMIN) {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()

View file

@ -4,6 +4,7 @@ import de.kif.backend.Configuration
import de.kif.backend.repository.PostRepository
import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.view.respondMain
import de.kif.common.formatDateTime
import de.kif.common.model.Schedule
import io.ktor.routing.Route
import io.ktor.routing.get
@ -11,11 +12,10 @@ import kotlinx.css.CSSBuilder
import kotlinx.css.Color
import kotlinx.html.div
import kotlinx.html.span
import kotlinx.html.unsafe
import java.util.*
fun Route.board() {
get("/brett") {
get("/board") {
val postList = PostRepository.all().asReversed()
val scheduleList = ScheduleRepository.all().map {
it to it.getAbsoluteStartTime() * 60
@ -24,9 +24,8 @@ fun Route.board() {
val referenceTime = Configuration.Schedule.referenceDate.time / 1000
val now = referenceTime - (Date().time / 1000)
respondMain(true, true) { theme ->
respondMain(true, true) {
content {
/*
div("board-header") {
div {
+"KIF 47.0"
@ -35,96 +34,46 @@ fun Route.board() {
+formatDateTime(Date().time)
}
}
*/
div("board") {
div("board-schedules") {
attributes["data-reference"] = referenceTime.toString()
div("board-header") {
+"AKs"
}
for ((schedule, time) in scheduleList) {
div("board-schedule") {
attributes["data-id"] = schedule.id.toString()
div("board-schedule-box") {
for ((schedule, time) in scheduleList) {
div("board-card board-schedule") {
attributes["data-id"] = schedule.id.toString()
span("board-schedule-room") {
+schedule.room.name
}
span("board-schedule-time") {
attributes["data-time"] = time.toString()
attributes["data-duration"] = schedule.workGroup.length.toString()
span("board-schedule-color") {
attributes["style"] = CSSBuilder().apply {
val c = schedule.workGroup.track?.color
if (c != null) {
backgroundColor = Color(c.toString())
}
}.toString()
}
span("board-schedule-name") {
+schedule.workGroup.name
}
div("board-schedule-bottom") {
span("board-schedule-time") {
attributes["data-time"] = time.toString()
attributes["data-duration"] = schedule.workGroup.length.toString()
val startTime = (time % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val sm = (startTime % 60).toString().padStart(2, '0')
val sh = (startTime / 60).toString().padStart(2, '0')
val startTimeString = "$sh:$sm"
val endTime = ((time + schedule.workGroup.length) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val em = (endTime % 60).toString().padStart(2, '0')
val eh = (endTime / 60).toString().padStart(2, '0')
val endTimeString = "$eh:$em"
+"$startTimeString - $endTimeString"
+Schedule.timeDifferenceToString(time + now)
}
span("board-schedule-color") {
attributes["style"] = CSSBuilder().apply {
val c = schedule.workGroup.track?.color
if (c != null) {
backgroundColor = Color(c.toString())
}
span("board-schedule-room") {
+schedule.room.name
}
}
}.toString()
}
span("board-schedule-name") {
+schedule.workGroup.name
}
}
}
}
div("board-posts") {
div("board-header") {
+"News"
}
for (post in postList) {
createPost(post, false, "board-card board-post")
createPost(post, false, "board-post overview-post")
}
}
div("board-twitter") {
div("board-header") {
+"Tweets"
}
div("board-card") {
unsafe {
raw(
"""
<a
class="twitter-timeline"
href="${Configuration.Twitter.timeline}"
data-chrome="transparent noheader nofooter"
data-theme="${if (theme.dark) "dark" else "light"}"
data-link-color="${theme.primaryColor}"
data-cards="hidden"
data-lang="de"
data-dnt="true"
>Tweets by kiforbiter</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
""".trimIndent()
)
}
}
}
}
}

View file

@ -77,7 +77,7 @@ fun Route.overview() {
val postList = PostRepository.all().asReversed()
respondMain { theme ->
respondMain {
content {
div("overview") {
div("overview-main") {
@ -94,21 +94,7 @@ fun Route.overview() {
}
}
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"
>Tweets by kiforbiter</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
""".trimIndent())
}
+"The Twitter Wall"
}
}
}

View file

@ -2,6 +2,7 @@ package de.kif.backend.view
import de.kif.backend.PortalSession
import de.kif.backend.Resources
import de.kif.backend.authenticate
import de.kif.common.model.User
import io.ktor.application.ApplicationCall
import io.ktor.application.call
@ -33,7 +34,7 @@ class MainTemplate(
link(href = "/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "/static/external/font/Montserrat.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(
href = "https://fonts.googleapis.com/css?family=Bungee|Oswald|Raleway",
href = "https://fonts.googleapis.com/css?family=Bungee|Oswald",
type = LinkType.textCss,
rel = LinkRel.stylesheet
)
@ -49,9 +50,6 @@ class MainTemplate(
Theme.PRINCESS -> {
link(href = "/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
}
Theme.BRETT -> {
link(href = "/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
}
}
script(src = "/static/require.min.js") {}
@ -81,7 +79,8 @@ class MainTemplate(
div("footer-credit") {
}
div("footer-theme") {
for ((it, name) in Theme.displayThemes) {
for (it in Theme.values()) {
val name = it.name.toLowerCase()
a("?theme=${it.name}", classes = if (theme == it) "selected" else "") {
id = "theme-$name"
+name.capitalize()
@ -95,19 +94,14 @@ class MainTemplate(
}
}
enum class Theme(val display: Boolean, val dark: Boolean, val primaryColor: String) {
LIGHT(true, false, "#B11D33"),
DARK(true, true, "#ef5350"),
PRINCESS(true, false, "#B11D33"),
BRETT(false, false, "#B11D33");
enum class Theme {
LIGHT, DARK, PRINCESS;
companion object {
private val lookup = values().toList().associateBy { it.name }
val displayThemes = values().filter { it.display }.map { it to it.name.toLowerCase() }
private val loopup = values().toList().associateBy { it.name }
fun lookup(name: String?): Theme {
return lookup[(name ?: return LIGHT).toUpperCase()] ?: LIGHT
return loopup[(name ?: return LIGHT).toUpperCase()] ?: LIGHT
}
}
}
@ -115,7 +109,7 @@ enum class Theme(val display: Boolean, val dark: Boolean, val primaryColor: Stri
suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
noMenu: Boolean = false,
stretch: Boolean = false,
body: MainTemplate.(Theme) -> Unit
body: MainTemplate.() -> Unit
) {
val param = call.request.queryParameters["theme"]
val url = call.request.uri.substring(1)
@ -130,17 +124,15 @@ suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
)
call.respondRedirect(call.request.path())
} else {
val theme = Theme.lookup(call.request.cookies["theme"])
call.respondHtmlTemplate(
MainTemplate(
theme,
Theme.lookup(call.request.cookies["theme"]),
url,
user,
noMenu,
stretch
)
) {
body(theme)
}
),
body = body
)
}
}

View file

@ -0,0 +1,5 @@
package twitter4j
fun addTwitterStreamListener(stream: TwitterStream, listener: StatusListener) {
stream.addListener(listener)
}