Add "April Fools' Day Mode" client preference
authorLanius Trolling <lanius@laniustrolling.dev>
Mon, 1 Apr 2024 23:21:41 +0000 (19:21 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Mon, 1 Apr 2024 23:21:41 +0000 (19:21 -0400)
src/jvmMain/kotlin/info/mechyrdia/lore/april_1st.kt
src/jvmMain/kotlin/info/mechyrdia/lore/view_map.kt
src/jvmMain/kotlin/info/mechyrdia/lore/view_tpl.kt
src/jvmMain/kotlin/info/mechyrdia/lore/views_prefs.kt
src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt
src/jvmMain/resources/static/init.js

index b0dece931eac99794ab5937e3c8bef2b0cda08ec..21e03b883b314f7e91b5cf11bd2be5b4675815dd 100644 (file)
@@ -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))
+}
index 3d1f929e8a3f3cbdef3f31259fcb5b3ffd6caae3..4306a610a6f13855b10e5a41a50169b2ed1de01c 100644 (file)
@@ -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")
index 2ffeffb37d10f4dcda9fb3295e6da9011baa198d..c598802fc7464dbc8becddfa5e572b136a1287c8 100644 (file)
@@ -33,14 +33,8 @@ val preloadImages = listOf(
 )
 
 fun ApplicationCall.page(pageTitle: String, navBar: List<NavItem>? = 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"
                
index f6f983dd4e936c044374909795178a5fffb538f6..30c63d781542cbe522b44a95b1a66ae349c5019e 100644 (file)
@@ -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>(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>(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"
+                       }
+               }
        }
 }
index a3886abede2a0773af4ae23781a68ad474055b87..10d0912fbdcb305aeac93b876bdc9f6170970456 100644 (file)
@@ -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)
                }
        }
        
index 1fc25efebf9123b71bb484f3a8586bae99798835..4a6646048d5b42a7f288a167d2158570d10a0a11 100644 (file)
        });
 
        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();
                        });
                }
        });