import starshipfights.DiscordLogin
import starshipfights.data.Id
import starshipfights.data.admiralty.*
+import starshipfights.data.auth.PreferredTheme
import starshipfights.data.auth.User
import starshipfights.data.auth.UserSession
import starshipfights.data.createNonce
showUserStatus = form["showstatus"] == "yes",
logIpAddresses = form["logaddress"] == "yes",
profileName = form["name"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_NAME_MAX_LENGTH } ?: redirect("/me/manage" + withErrorMessage("Invalid name - must not be blank, must be at most $PROFILE_NAME_MAX_LENGTH characters")),
- profileBio = form["bio"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_BIO_MAX_LENGTH } ?: redirect("/me/manage" + withErrorMessage("Invalid bio - must not be blank, must be at most $PROFILE_BIO_MAX_LENGTH characters"))
+ profileBio = form["bio"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_BIO_MAX_LENGTH } ?: redirect("/me/manage" + withErrorMessage("Invalid bio - must not be blank, must be at most $PROFILE_BIO_MAX_LENGTH characters")),
+ preferredTheme = form["theme"]?.uppercase()?.takeIf { it in PreferredTheme.values().map { it.name } }?.let { PreferredTheme.valueOf(it) } ?: currentUser.preferredTheme
)
User.put(newUser)
if (call.getUserSession() != null)
redirect("/me")
- call.respondHtml(HttpStatusCode.OK, page("Authentication Test", call.standardNavBar(), CustomSidebar {
+ call.respondHtml(HttpStatusCode.OK, call.page("Authentication Test", call.standardNavBar(), CustomSidebar {
p {
+"This instance does not have Discord OAuth login set up. As a fallback, this authentication mode is used for testing only."
}
override fun installRouting(conf: Routing) {
with(conf) {
get("/login") {
- call.respondHtml(HttpStatusCode.OK, page("Login with Discord", call.standardNavBar()) {
+ call.respondHtml(HttpStatusCode.OK, call.page("Login with Discord", call.standardNavBar()) {
section {
p {
style = "text-align:center"
lastActivity = Instant.now()
).also { User.put(it) }
-suspend fun ApplicationCall.getUserSession() = request.userAgent()?.let { sessions.get<Id<UserSession>>()?.resolve(it) }?.renewed(request.origin.remoteHost)
+val UserAndSessionAttribute = AttributeKey<Pair<UserSession?, User?>>("SfUserAndSession")
-suspend fun ApplicationCall.getUser() = getUserSession()?.user?.let { User.get(it) }?.updated()
+suspend fun ApplicationCall.getUserSession() = getUserAndSession().first
-suspend fun ApplicationCall.getUserAndSession() = getUserSession()?.let { it to User.get(it.user)?.updated() } ?: (null to null)
+suspend fun ApplicationCall.getUser() = getUserAndSession().second
+
+suspend fun ApplicationCall.getUserAndSession() = request.pipeline.attributes.getOrNull(UserAndSessionAttribute)
+ ?: request.userAgent()?.let { sessions.get<Id<UserSession>>()?.resolve(it) }
+ ?.renewed(request.origin.remoteHost)
+ ?.let { it to User.get(it.user)?.updated() }
+ ?.also { request.pipeline.attributes.put(UserAndSessionAttribute, it) }
+ ?: (null to null)
object UserSessionIdSerializer : SessionSerializer<Id<UserSession>> {
override fun serialize(session: Id<UserSession>): String {
val profileName: String,
val profileBio: String,
+ val preferredTheme: PreferredTheme = PreferredTheme.SYSTEM,
+
val registeredAt: @Contextual Instant,
val lastActivity: @Contextual Instant,
val showUserStatus: Boolean,
})
}
+enum class PreferredTheme {
+ SYSTEM, LIGHT, DARK;
+}
+
enum class UserStatus {
AVAILABLE, IN_MATCHMAKING, READY_FOR_BATTLE, IN_BATTLE
}
package starshipfights.info
+import io.ktor.application.*
import kotlinx.html.*
+import starshipfights.auth.getUser
+import starshipfights.data.auth.PreferredTheme
-fun page(pageTitle: String? = null, navBar: List<NavItem>? = null, sidebar: Sidebar? = null, content: SECTIONS.() -> Unit): HTML.() -> Unit = {
- head {
- meta(charset = "utf-8")
- meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
+suspend fun ApplicationCall.page(pageTitle: String? = null, navBar: List<NavItem>? = null, sidebar: Sidebar? = null, content: SECTIONS.() -> Unit): HTML.() -> Unit {
+ val theme = getUser()?.preferredTheme
+
+ return {
+ when (theme) {
+ PreferredTheme.LIGHT -> "light"
+ PreferredTheme.DARK -> "dark"
+ else -> null
+ }?.let { attributes["data-theme"] = it }
- link(rel = "icon", type = "image/svg+xml", href = "/static/images/icon.svg")
- link(rel = "preconnect", href = "https://fonts.googleapis.com")
- link(rel = "preconnect", href = "https://fonts.gstatic.com") { attributes["crossorigin"] = "anonymous" }
- link(rel = "stylesheet", href = "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Jetbrains+Mono:wght@400;600;800&display=swap")
- link(rel = "stylesheet", href = "/static/style.css")
-
- title {
- +"Starship Fights"
- pageTitle?.let { +" | $it" }
+ head {
+ meta(charset = "utf-8")
+ meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
+
+ link(rel = "icon", type = "image/svg+xml", href = "/static/images/icon.svg")
+ link(rel = "preconnect", href = "https://fonts.googleapis.com")
+ link(rel = "preconnect", href = "https://fonts.gstatic.com") { attributes["crossorigin"] = "anonymous" }
+ link(rel = "stylesheet", href = "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Jetbrains+Mono:wght@400;600;800&display=swap")
+ link(rel = "stylesheet", href = "/static/style.css")
+
+ title {
+ +"Starship Fights"
+ pageTitle?.let { +" | $it" }
+ }
}
- }
- body {
- div { id = "bg" }
-
- navBar?.let { nb ->
- nav(classes = "desktop") {
- div(classes = "list") {
- for (ni in nb) {
- div(classes = "item") {
- ni.displayIn(this)
+ body {
+ div { id = "bg" }
+
+ navBar?.let { nb ->
+ nav(classes = "desktop") {
+ div(classes = "list") {
+ for (ni in nb) {
+ div(classes = "item") {
+ ni.displayIn(this)
+ }
}
}
}
}
- }
-
- sidebar?.let {
- aside(classes = "desktop") {
- it.displayIn(this)
- }
- }
-
- main {
+
sidebar?.let {
- aside(classes = "mobile") {
+ aside(classes = "desktop") {
it.displayIn(this)
}
}
- with(sectioned()) {
- content()
- }
-
- navBar?.let { nb ->
- nav(classes = "mobile") {
- div(classes = "list") {
- for (ni in nb) {
- div(classes = "item") {
- ni.displayIn(this)
+ main {
+ sidebar?.let {
+ aside(classes = "mobile") {
+ it.displayIn(this)
+ }
+ }
+
+ with(sectioned()) {
+ content()
+ }
+
+ navBar?.let { nb ->
+ nav(classes = "mobile") {
+ div(classes = "list") {
+ for (ni in nb) {
+ div(classes = "item") {
+ ni.displayIn(this)
+ }
}
}
}
}
}
+
+ script(src = "/static/init.js") {}
}
-
- script(src = "/static/init.js") {}
}
}
appendLine("Profile bio: \"\"\"")
appendLine(user.profileBio)
appendLine("\"\"\"")
+ appendLine("Display theme: ${user.preferredTheme}")
appendLine("")
appendLine("## Activity data")
appendLine("Registered at: ${user.registeredAt}")
+currentUser.profileBio
}
+ h3 {
+ +"Display Theme"
+ }
+ label {
+ radioInput(name = "theme") {
+ id = "system-theme"
+ value = "system"
+ required = true
+ checked = currentUser.preferredTheme == PreferredTheme.SYSTEM
+ }
+ +Entities.nbsp
+ +"System Choice"
+ }
+ br
+ label {
+ radioInput(name = "theme") {
+ id = "light-theme"
+ value = "light"
+ required = true
+ checked = currentUser.preferredTheme == PreferredTheme.LIGHT
+ }
+ +Entities.nbsp
+ +"Light Theme"
+ }
+ br
+ label {
+ radioInput(name = "theme") {
+ id = "dark-theme"
+ value = "dark"
+ required = true
+ checked = currentUser.preferredTheme == PreferredTheme.DARK
+ }
+ +Entities.nbsp
+ +"Dark Theme"
+ }
h3 {
+"Privacy Settings"
}
value = "Accept Changes"
}
}
+ script {
+ unsafe { +"window.sfThemeChoice = true;" }
+ }
}
section {
h2 { +"Logged-In Sessions" }
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ viewBox="1 0 12 12"
+ height="16"
+ width="16">
+ <path
+ style="fill:#6699ff;fill-opacity:1"
+ id="path12"
+ d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
+ <path
+ style="fill:#5588ee;fill-opacity:1"
+ id="path10"
+ d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
+ <path
+ style="fill:#4477dd;fill-opacity:1"
+ id="path14"
+ d="M4.082 5.51c0-.621.621-.621.621-.621 1.864.621 3.107 1.864 3.728 3.728 0 0 0 .621-.62.621-1.245-1.864-1.866-2.485-3.73-3.728z"/>
+</svg>
height="16"
width="16">
<path
- style="fill:#2255bb;fill-opacity:1"
+ style="fill:#003399;fill-opacity:1"
id="path12"
d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
<path
- style="fill:#4477dd;fill-opacity:1"
+ style="fill:#3366cc;fill-opacity:1"
id="path10"
d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
<path
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ viewBox="0 0 64 64"
+ height="64px"
+ width="64px">
+ <path
+ d="M 24,8 A 16,16 0 0 1 8,24 v 16 A 16,16 0 0 1 24,56 H 40 A 16,16 0 0 1 56,40 V 24 A 16,16 0 0 1 40,8 Z"
+ style="fill:#333333;opacity:0.9;stroke:none"/>
+ <path
+ d="M 20 4 L 20 8 C 20 14.67 14.67 20 8 20 L 4 20 L 4 44 L 8 44 C 14.67 44 20 49.33 20 56 L 20 60 L 44 60 L 44 56 C 44 49.33 49.33 44 56 44 L 60 44 L 60 20 L 56 20 C 49.33 20 44 14.67 44 8 L 44 4 L 20 4 z M 24 8 L 40 8 A 16 16 0 0 0 56 24 L 56 40 A 16 16 0 0 0 40 56 L 24 56 A 16 16 0 0 0 8 40 L 8 24 A 16 16 0 0 0 24 8 z"
+ style="fill:#99ccff;opacity:1;fill-rule:evenodd;stroke:none"/>
+</svg>
if (!window.sfFactionSelect) return;
const factionInputs = document.getElementsByName("faction");
+ const sexInputs = document.getElementsByName("sex");
for (const factionInput of factionInputs) {
factionInput.addEventListener("click", () => {
- const sexInputs = document.getElementsByName("sex");
if (factionInput.hasAttribute("data-force-gender")) {
const forceGender = factionInput.getAttribute("data-force-gender");
for (const sexInput of sexInputs) {
};
}
});
+
+ window.addEventListener("load", function () {
+ // Generate random admiral names
+ if (!window.sfThemeChoice) return;
+
+ const themeChoices = document.getElementsByName("theme");
+ for (let themeChoice of themeChoices) {
+ const theme = themeChoice.value;
+ themeChoice.addEventListener("click", () => {
+ document.documentElement.setAttribute("data-theme", theme);
+ });
+ }
+ });
})();
--h1-size: 1.6em;
--h2-size: 1.4em;
--h3-size: 1.2em;
+
+ /***************
+ * color params *
+ ***************/
+
+ --selection-fg: #eee;
+ --selection-bg: rgba(51, 102, 204, 0.6);
+
+ --h1-border: #888;
+ --h1-shadow: #444;
+ --h1-backgr: #aaa;
+
+ --h2-border: #666;
+
+ --h3-unline: #888;
+
+ --list-a-fg: #369;
+ --list-a-h-fg: #fff;
+ --list-a-h-bg: #69c;
+
+ --a-fg: #36c;
+ --a-v-fg: #63c;
+
+ --tbl-border: #036;
+ --tbl-backgr: #eee;
+
+ --input-bg: #bbb;
+ --input-ul: #222;
+ --input-fg: #024;
+ --input-f-bg: #777;
+
+ --err-bg: #e77;
+ --err-fg: #422;
+ --err-ul: #211;
+
+ --btn-fg: #bdf;
+ --btn-bg: #06c;
+ --btn-h-bg: #05a;
+ --btn-a-bg: #048;
+
+ --evil-btn-fg: #fcc;
+ --evil-btn-bg: #c33;
+ --evil-btn-h-bg: #a22;
+ --evil-btn-a-bg: #811;
+
+ --btn-na-bg: #444;
+ --btn-na-fg: #bbb;
+
+ --iframe-border: #036;
+
+ /*************
+ * url params *
+ *************/
+
+ --panel: url("/static/images/panel.svg");
+ --bgimg: url("/static/images/background.jpg");
+ --extln: url("/static/images/external-link.svg");
+}
+
+html[data-theme="dark"] {
+ color: #ddd;
+ background-color: #555;
+
+ /***************
+ * color params *
+ ***************/
+
+ --selection-fg: #111;
+ --selection-bg: rgba(102, 153, 255, 0.9);
+
+ --h1-border: #333;
+ --h1-shadow: #bbb;
+ --h1-backgr: #111;
+
+ --h2-border: #999;
+
+ --h3-unline: #777;
+
+ --list-a-fg: #9cf;
+ --list-a-h-fg: #333;
+ --list-a-h-bg: #9cf;
+
+ --a-fg: #69f;
+ --a-v-fg: #96f;
+
+ --tbl-border: #9cf;
+ --tbl-backgr: #111;
+
+ --input-bg: #444;
+ --input-ul: #ddd;
+ --input-fg: #bdf;
+ --input-f-bg: #888;
+
+ --err-bg: #844;
+ --err-fg: #fcc;
+ --err-ul: #422;
+
+ --btn-fg: #024;
+ --btn-bg: #39f;
+ --btn-h-bg: #7bf;
+ --btn-a-bg: #bdf;
+
+ --evil-btn-fg: #411;
+ --evil-btn-bg: #d33;
+ --evil-btn-h-bg: #e66;
+ --evil-btn-a-bg: #f99;
+
+ --btn-na-bg: #bbb;
+ --btn-na-fg: #444;
+
+ --iframe-border: #9cf;
+
+ /*************
+ * url params *
+ *************/
+
+ --panel: url("/static/images/panel-dark.svg");
+ --bgimg: url("/static/images/background-dark.jpg");
+ --extln: url("/static/images/external-link-dark.svg");
+}
+
+@media only screen and (prefers-color-scheme: dark) {
+ html[data-theme="light"] {
+ color: #222;
+ background-color: #aaa;
+
+ /***************
+ * color params *
+ ***************/
+
+ --selection-fg: #eee;
+ --selection-bg: rgba(51, 102, 204, 0.6);
+
+ --h1-border: #888;
+ --h1-shadow: #444;
+ --h1-backgr: #aaa;
+
+ --h2-border: #666;
+
+ --h3-unline: #888;
+
+ --list-a-fg: #369;
+ --list-a-h-fg: #fff;
+ --list-a-h-bg: #69c;
+
+ --a-fg: #36c;
+ --a-v-fg: #63c;
+
+ --tbl-border: #036;
+ --tbl-backgr: #eee;
+
+ --input-bg: #bbb;
+ --input-ul: #222;
+ --input-fg: #024;
+ --input-f-bg: #777;
+
+ --err-bg: #e77;
+ --err-fg: #422;
+ --err-ul: #211;
+
+ --btn-fg: #bdf;
+ --btn-bg: #06c;
+ --btn-h-bg: #05a;
+ --btn-a-bg: #048;
+
+ --evil-btn-fg: #fcc;
+ --evil-btn-bg: #c33;
+ --evil-btn-h-bg: #a22;
+ --evil-btn-a-bg: #811;
+
+ --btn-na-bg: #444;
+ --btn-na-fg: #bbb;
+
+ --iframe-border: #036;
+
+ /*************
+ * url params *
+ *************/
+
+ --panel: url("/static/images/panel.svg");
+ --bgimg: url("/static/images/background.jpg");
+ --extln: url("/static/images/external-link.svg");
+ }
+
+ html {
+ color: #ddd;
+ background-color: #555;
+
+ /***************
+ * color params *
+ ***************/
+
+ --selection-fg: #111;
+ --selection-bg: rgba(102, 153, 255, 0.9);
+
+ --h1-border: #333;
+ --h1-shadow: #bbb;
+ --h1-backgr: #111;
+
+ --h2-border: #999;
+
+ --h3-unline: #777;
+
+ --list-a-fg: #9cf;
+ --list-a-h-fg: #333;
+ --list-a-h-bg: #9cf;
+
+ --a-fg: #69f;
+ --a-v-fg: #96f;
+
+ --tbl-border: #9cf;
+ --tbl-backgr: #111;
+
+ --input-bg: #444;
+ --input-ul: #ddd;
+ --input-fg: #bdf;
+ --input-f-bg: #888;
+
+ --err-bg: #844;
+ --err-fg: #fcc;
+ --err-ul: #422;
+
+ --btn-fg: #024;
+ --btn-bg: #39f;
+ --btn-h-bg: #5af;
+ --btn-a-bg: #7bf;
+
+ --evil-btn-fg: #411;
+ --evil-btn-bg: #d33;
+ --evil-btn-h-bg: #e66;
+ --evil-btn-a-bg: #f99;
+
+ --btn-na-bg: #bbb;
+ --btn-na-fg: #444;
+
+ --iframe-border: #9cf;
+
+ /*************
+ * url params *
+ *************/
+
+ --panel: url("/static/images/panel-dark.svg");
+ --bgimg: url("/static/images/background-dark.jpg");
+ --extln: url("/static/images/external-link-dark.svg");
+ }
}
::selection {
- background-color: rgb(51, 102, 204, 0.6);
- color: #eee;
+ background-color: var(--selection-bg);
+ color: var(--selection-fg);
}
div#bg {
}
h1 {
- border: 0.1875rem solid #888;
- box-shadow: inset 0 0 0 0.25rem #444;
+ border: 0.1875rem solid var(--h1-border);
+ box-shadow: inset 0 0 0 0.25rem var(--h1-shadow);
padding: 0.3125rem;
- background-color: #aaa;
+ background-color: var(--h1-backgr);
font-variant: small-caps;
font-size: var(--h1-size);
font-weight: 800;
}
h2 {
- border-bottom: 0.125rem solid #666;
+ border-bottom: 0.125rem solid var(--h2-border);
font-size: var(--h2-size);
font-weight: 600;
}
h3 {
text-decoration: underline;
- text-decoration-color: #888;
+ text-decoration-color: var(--h3-unline);
font-size: var(--h3-size);
font-weight: 400;
}
/*noinspection CssOverwrittenProperties*/
main > section, main > nav.mobile, main > aside.mobile {
- border-image-source: url("/static/images/panel.svg");
+ border-image-source: var(--panel);
border-image-slice: 40% fill;
border-image-width: 1em;
border-width: 1em;
top: 0;
left: 0;
- background-image: url("/static/images/background.jpg");
+ background-image: var(--bgimg);
background-attachment: fixed;
background-position: center;
background-size: cover;
/*noinspection CssOverwrittenProperties*/
main > section, nav.desktop, aside.desktop {
- border-image-source: url("/static/images/panel.svg");
+ border-image-source: var(--panel);
border-image-slice: 40% fill;
border-image-width: 2em;
border-width: 2em;
text-align: center;
border-radius: 0.3em;
- color: #369;
+ color: var(--list-a-fg);
text-decoration: none;
}
div.list > div.item > a:visited {
- color: #369;
+ color: var(--list-a-fg);
}
div.list > div.item > a:hover {
- color: #fff;
- background-color: #69c;
+ color: var(--list-a-h-fg);
+ background-color: var(--list-a-h-bg);
}
a {
- color: #36c;
+ color: var(--a-fg);
text-decoration: none;
}
a:visited {
- color: #63c;
+ color: var(--a-v-fg);
}
a:hover {
}
a[href^="http://"]::after, a[href^="https://"]::after {
- content: url(/static/images/external-link.svg);
+ content: var(--extln);
height: 1em;
}
table {
table-layout: fixed;
border-collapse: collapse;
- border: 0.125rem solid #036;
- background-color: #ccc;
+ border: 0.125rem solid var(--tbl-border);
width: 100%;
}
}
td {
- border: 0.125rem solid #036;
- background-color: #eee;
+ border: 0.125rem solid var(--tbl-border);
+ background-color: var(--tbl-backgr);
font-size: 0.7em;
padding: 0.15em 0;
}
th {
- border: 0.125rem solid #036;
- background-color: #036;
+ border: 0.125rem solid var(--tbl-border);
+ background-color: var(--tbl-border);
padding: 0.15em 0;
text-align: center;
font-size: 0.8em;
font-variant: small-caps;
font-weight: 700;
- color: #eee;
+ color: var(--tbl-backgr);
}
input[type=text],
input[type=email],
textarea {
box-sizing: border-box;
- background-color: #aaa;
+ background-color: var(--input-bg);
border: none;
- border-bottom: 0.1em solid #222;
+ border-bottom: 0.1em solid var(--input-ul);
- color: #024;
+ color: var(--input-fg);
font-size: 1.5em;
width: 100%;
input[type=email]:focus,
textarea:focus {
outline: none;
- background-color: #888;
+ background-color: var(--input-f-bg);
}
input[type=text]:invalid,
input[type=password]:invalid,
input[type=email]:invalid,
textarea:invalid {
- color: #422;
- background-color: #e77;
- border-bottom-color: #211;
+ color: var(--err-fg);
+ background-color: var(--err-bg);
+ border-bottom-color: var(--err-ul);
}
input[type=submit] {
- background-color: #06c;
+ background-color: var(--btn-bg);
border: none;
border-radius: 0.3em;
- color: #bdf;
+ color: var(--btn-fg);
cursor: pointer;
display: block;
}
input[type=submit]:hover {
- background-color: #05a;
+ background-color: var(--btn-h-bg);
}
input[type=submit]:active {
- background-color: #048;
+ background-color: var(--btn-a-bg);
}
input[type=submit].evil {
- background-color: #c33;
+ background-color: var(--evil-btn-bg);
border: none;
border-radius: 0.3em;
- color: #fcc;
+ color: var(--evil-btn-fg);
cursor: pointer;
display: block;
}
input[type=submit].evil:hover {
- background-color: #a22;
+ background-color: var(--evil-btn-h-bg);
}
input[type=submit].evil:active {
- background-color: #811;
+ background-color: var(--evil-btn-a-bg);
}
input[type=submit]:disabled,
input[type=submit].evil:disabled {
- background-color: #444;
+ background-color: var(--btn-na-bg);
+ color: var(--btn-na-fg);
cursor: not-allowed;
}
iframe {
- border-color: #036;
+ border-color: var(--iframe-border);
}