From 3d0de9f268fbc45c67e0bdb454b97f0f8bc4d31e Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Mon, 1 Apr 2024 19:21:41 -0400 Subject: [PATCH] Add "April Fools' Day Mode" client preference --- .../kotlin/info/mechyrdia/lore/april_1st.kt | 9 +- .../kotlin/info/mechyrdia/lore/view_map.kt | 8 +- .../kotlin/info/mechyrdia/lore/view_tpl.kt | 8 +- .../kotlin/info/mechyrdia/lore/views_prefs.kt | 97 ++++++++++++++++--- .../info/mechyrdia/route/resource_types.kt | 2 +- src/jvmMain/resources/static/init.js | 12 ++- 6 files changed, 110 insertions(+), 26 deletions(-) diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/april_1st.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/april_1st.kt index b0dece9..21e03b8 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/april_1st.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/april_1st.kt @@ -1,6 +1,7 @@ package info.mechyrdia.lore import info.mechyrdia.Configuration +import io.ktor.server.application.* import io.ktor.util.* import java.io.File import java.time.Instant @@ -14,8 +15,9 @@ fun isApril1st(time: Instant = Instant.now()): Boolean { return zonedDateTime.month == Month.APRIL && zonedDateTime.dayOfMonth == 1 } +context(ApplicationCall) fun redirectFileOnApril1st(requestedFile: File): File? { - if (!isApril1st()) return null + if (!april1stMode.isEnabled) return null val rootDir = File(Configuration.CurrentConfiguration.rootDir) val requestedPath = requestedFile.absoluteFile.toRelativeString(rootDir.absoluteFile) @@ -23,6 +25,11 @@ fun redirectFileOnApril1st(requestedFile: File): File? { return funnyFile.takeIf { it.exists() } } +context(ApplicationCall) fun getAssetFile(requestedFile: File): File { return redirectFileOnApril1st(requestedFile) ?: requestedFile } + +suspend fun ApplicationCall.respondAsset(assetFile: File) { + respondCompressedFile(getAssetFile(assetFile)) +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_map.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/view_map.kt index 3d1f929..4306a61 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/view_map.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/view_map.kt @@ -6,10 +6,10 @@ import io.ktor.util.* import java.io.File fun ApplicationCall.galaxyMapPage(): File { - val themeName = when (request.cookies["FACTBOOK_THEME"]) { - "light" -> "light" - "dark" -> "dark" - else -> "system" + val themeName = when (pageTheme) { + PageTheme.SYSTEM -> "system" + PageTheme.LIGHT -> "light" + PageTheme.DARK -> "dark" } return File(Configuration.CurrentConfiguration.assetDir).combineSafe("map/index-$themeName.html") diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt index 2ffeffb..c598802 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt @@ -33,14 +33,8 @@ val preloadImages = listOf( ) fun ApplicationCall.page(pageTitle: String, navBar: List? = null, sidebar: Sidebar? = null, ogData: OpenGraphData? = null, content: SECTIONS.() -> Unit): HTML.() -> Unit { - val theme = request.cookies["FACTBOOK_THEME"] - return { - when (theme) { - "light" -> "light" - "dark" -> "dark" - else -> null - }?.let { attributes["data-theme"] = it } + pageTheme.attributeValue?.let { attributes["data-theme"] = it } lang = "en" diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_prefs.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/views_prefs.kt index f6f983d..30c63d7 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/views_prefs.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/views_prefs.kt @@ -1,27 +1,72 @@ package info.mechyrdia.lore import info.mechyrdia.auth.PageDoNotCacheAttributeKey +import info.mechyrdia.route.KeyedEnumSerializer import io.ktor.server.application.* import kotlinx.html.* +import kotlinx.serialization.Serializable + +@Serializable(PageThemeSerializer::class) +enum class PageTheme(val attributeValue: String?) { + SYSTEM(null), + LIGHT("light"), + DARK("dark"); +} + +object PageThemeSerializer : KeyedEnumSerializer(PageTheme.entries, PageTheme::attributeValue) + +val ApplicationCall.pageTheme: PageTheme + get() = when (request.cookies["FACTBOOK_THEME"]) { + "light" -> PageTheme.LIGHT + "dark" -> PageTheme.DARK + else -> PageTheme.SYSTEM + } + +@Serializable(with = April1stModeSerializer::class) +enum class April1stMode { + DEFAULT { + override val isEnabled: Boolean + get() = isApril1st() + }, + ALWAYS { + override val isEnabled: Boolean + get() = true + }, + NEVER { + override val isEnabled: Boolean + get() = false + }; + + abstract val isEnabled: Boolean +} + +object April1stModeSerializer : KeyedEnumSerializer(April1stMode.entries) + +val ApplicationCall.april1stMode: April1stMode + get() = when (request.cookies["APRIL_1ST_MODE"]) { + "always" -> April1stMode.ALWAYS + "never" -> April1stMode.NEVER + else -> April1stMode.DEFAULT + } suspend fun ApplicationCall.clientSettingsPage(): HTML.() -> Unit { attributes.put(PageDoNotCacheAttributeKey, true) - val theme = when (request.cookies["FACTBOOK_THEME"]) { - "light" -> "light" - "dark" -> "dark" - else -> null - } + val theme = pageTheme + val april1st = april1stMode return page("Client Preferences", standardNavBar()) { section { h1 { +"Client Preferences" } + p { +"This is the place where you can adjust your client preferences. Selecting an option changes it automatically, so you don't need to click any kind of \"save\" button. Also, note that preferences are saved per-browser in your cookies, so don't expect your client-side preferences to travel with you to other devices." } + } + section { + h2 { +"Page Theme" } label { radioInput(name = "theme") { - id = "system-theme" value = "system" required = true - checked = theme == null + checked = theme == PageTheme.SYSTEM } +Entities.nbsp +"System Choice" @@ -29,10 +74,9 @@ suspend fun ApplicationCall.clientSettingsPage(): HTML.() -> Unit { br label { radioInput(name = "theme") { - id = "light-theme" value = "light" required = true - checked = theme == "light" + checked = theme == PageTheme.LIGHT } +Entities.nbsp +"Light Theme" @@ -40,14 +84,45 @@ suspend fun ApplicationCall.clientSettingsPage(): HTML.() -> Unit { br label { radioInput(name = "theme") { - id = "dark-theme" value = "dark" required = true - checked = theme == "dark" + checked = theme == PageTheme.DARK } +Entities.nbsp +"Dark Theme" } } + section { + h2 { +"April Fools' Day Mode" } + label { + radioInput(name = "april1st") { + value = "default" + required = true + checked = april1st == April1stMode.DEFAULT + } + +Entities.nbsp + +"Only on April 1st" + } + br + label { + radioInput(name = "april1st") { + value = "always" + required = true + checked = april1st == April1stMode.ALWAYS + } + +Entities.nbsp + +"Always" + } + br + label { + radioInput(name = "april1st") { + value = "never" + required = true + checked = april1st == April1stMode.NEVER + } + +Entities.nbsp + +"Never" + } + } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt b/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt index a3886ab..10d0912 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt @@ -39,7 +39,7 @@ class Root(val error: String? = null) : ResourceHandler, ResourceFilter { val assetPath = path.joinToString(separator = File.separator) val assetFile = File(Configuration.CurrentConfiguration.assetDir).combineSafe(assetPath) - call.respondCompressedFile(getAssetFile(assetFile)) + call.respondAsset(assetFile) } } diff --git a/src/jvmMain/resources/static/init.js b/src/jvmMain/resources/static/init.js index 1fc25ef..4a66460 100644 --- a/src/jvmMain/resources/static/init.js +++ b/src/jvmMain/resources/static/init.js @@ -163,13 +163,21 @@ }); window.addEventListener("load", function () { - // Set client theme when selected + // Set client preferences when selected const themeChoices = document.getElementsByName("theme"); for (const themeChoice of themeChoices) { themeChoice.addEventListener("click", e => { const theme = e.currentTarget.value; document.documentElement.setAttribute("data-theme", theme); - document.cookie = "FACTBOOK_THEME=" + theme + "; secure; max-age=" + (Math.pow(2, 31) - 1).toString(); + document.cookie = "FACTBOOK_THEME=" + theme + "; Secure; SameSite=Lax; Max-Age=" + (Math.pow(2, 31) - 1).toString(); + }); + } + + const april1stChoices = document.getElementsByName("april1st"); + for (const april1stChoice of april1stChoices) { + april1stChoice.addEventListener("click", e => { + const mode = e.currentTarget.value; + document.cookie = "APRIL_1ST_MODE=" + mode + "; Secure; SameSite=None; Max-Age=" + (Math.pow(2, 31) - 1).toString(); }); } }); -- 2.25.1