330 lines
14 KiB
Scala
330 lines
14 KiB
Scala
package views.html.base
|
|
|
|
import play.twirl.api.Html
|
|
|
|
import lila.api.Context
|
|
import lila.app.templating.Environment._
|
|
import lila.app.ui.ScalatagsTemplate._
|
|
import lila.common.ContentSecurityPolicy
|
|
|
|
import controllers.routes
|
|
|
|
object layout {
|
|
|
|
object bits {
|
|
val doctype = raw("<!doctype html>")
|
|
def htmlTag(implicit ctx: Context) = html(st.lang := ctx.lang.language)
|
|
val topComment = raw("""<!-- Lichess is open source! See https://github.com/ornicar/lila -->""")
|
|
val charset = raw("""<meta charset="utf-8">""")
|
|
val viewport = raw("""<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"/>""")
|
|
def metaCsp(csp: Option[ContentSecurityPolicy])(implicit ctx: Context): Option[Frag] =
|
|
cspEnabled() option raw(
|
|
s"""<meta http-equiv="Content-Security-Policy" content="${csp.getOrElse(defaultCsp)}">"""
|
|
)
|
|
def currentBgCss(implicit ctx: Context) = ctx.currentBg match {
|
|
case "dark" => cssTag("dark.css")
|
|
case "transp" => cssTags("dark.css", "transp.css")
|
|
case _ => emptyHtml
|
|
}
|
|
def pieceSprite(implicit ctx: Context) =
|
|
link(id := "piece-sprite", href := assetUrl(s"stylesheets/piece/${ctx.currentPieceSet}.css"), `type` := "text/css", rel := "stylesheet")
|
|
val fontStylesheets = raw(List(
|
|
"""<link href="https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Roboto:300" rel="stylesheet">""",
|
|
"""<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:500&text=0123456789:." rel="stylesheet">"""
|
|
).mkString)
|
|
}
|
|
import bits._
|
|
|
|
private val noTranslate = raw("""<meta name="google" content="notranslate" />""")
|
|
private val fontPreload = raw(s"""<link rel="preload" href="${assetUrl(s"font/lichess/fonts/lichess.woff")}" as="font" type="font/woff" crossorigin/>""")
|
|
private val manifests = raw(List(
|
|
"""<link rel="manifest" href="/manifest.json" />""",
|
|
"""<meta name="twitter:site" content="@lichess" />"""
|
|
).mkString)
|
|
|
|
private val favicons = raw {
|
|
List(256, 128, 64) map { px =>
|
|
s"""<link rel="icon" type="image/png" href="${staticUrl(s"favicon.$px.png")}" sizes="${px}x${px}"/>"""
|
|
} mkString
|
|
}
|
|
private def blindModeForm(implicit ctx: Context) = raw(s"""<form id="blind_mode" action="${routes.Main.toggleBlindMode}" method="POST"><input type="hidden" name="enable" value="${if (ctx.blind) 0 else 1}" /><input type="hidden" name="redirect" value="${ctx.req.path}" /><button type="submit">Accessibility: ${if (ctx.blind) "Disable" else "Enable"} blind mode</button></form>""")
|
|
private val zenToggle = raw("""<a data-icon="E" id="zentog" class="text fbt active">ZEN MODE</a>""")
|
|
private def dasher(me: lila.user.User) = raw(s"""<div class="dasher"><a id="user_tag" class="toggle link">${me.username}</a><div id="dasher_app" class="dropdown"></div></div>""")
|
|
|
|
private def allNotifications(implicit ctx: Context) = spaceless(s"""<div class="challenge_notifications">
|
|
<a id="challenge_notifications_tag" class="toggle link">
|
|
<span title="${trans.challenges.txt()}" class="data-count" data-count="${ctx.nbChallenges}" data-icon="U"></span>
|
|
</a>
|
|
<div id="challenge_app" class="dropdown"></div>
|
|
</div>
|
|
<div class="site_notifications">
|
|
<a id="site_notifications_tag" class="toggle link">
|
|
<span title="${trans.notifications.txt()}" class="data-count" data-count="${ctx.nbNotifications}" data-icon=""</span>
|
|
</a>
|
|
<div id="notify_app" class="dropdown"></div>
|
|
</div>""")
|
|
|
|
private def anonDasher(playing: Boolean)(implicit ctx: Context) = spaceless(s"""<div class="dasher">
|
|
<a class="toggle link anon">
|
|
<span title="${trans.preferences.txt()}" data-icon="%"</span>
|
|
</a>
|
|
<div id="dasher_app" class="dropdown" data-playing="$playing"></div>
|
|
</div>
|
|
<a href="${routes.Auth.login}?referrer=${currentPath}" class="signin button">${trans.signIn.txt()}</a>""")
|
|
|
|
private val clinputLink = a(cls := "link")(span(dataIcon := "y"))
|
|
|
|
private def clinput(implicit ctx: Context) =
|
|
div(id := "clinput")(
|
|
clinputLink,
|
|
input(spellcheck := "false", placeholder := trans.search.txt())
|
|
)
|
|
|
|
private lazy val botImage = img(src := staticUrl("images/icons/bot.png"), title := "Robot chess", style := "display:inline;width:34px;height:34px;vertical-align:top;margin-right:5px;")
|
|
|
|
private val spaceRegex = """\s{2,}+""".r
|
|
private def spaceless(html: String) = raw(spaceRegex.replaceAllIn(html.replace("\\n", ""), ""))
|
|
|
|
private val dataDev = attr("data-dev")
|
|
private val dataUser = attr("data-user")
|
|
private val dataSoundSet = attr("data-sound-set")
|
|
private val dataSocketDomain = attr("data-socket-domain")
|
|
private val dataAssetUrl = attr("data-asset-url")
|
|
private val dataAssetVersion = attr("data-asset-version")
|
|
private val dataNonce = attr("data-nonce")
|
|
private val dataZoom = attr("data-zoom")
|
|
private val dataTheme = attr("data-theme")
|
|
private val dataResp = attr("data-resp")
|
|
private val dataPreload = attr("data-preload")
|
|
private val dataPlaying = attr("data-playing")
|
|
private val dataPatrons = attr("data-patrons")
|
|
private val dataStudying = attr("data-studying")
|
|
|
|
def apply(
|
|
title: String,
|
|
fullTitle: Option[String] = None,
|
|
side: Option[Html] = None,
|
|
menu: Option[Html] = None,
|
|
chat: Option[Frag] = None,
|
|
underchat: Option[Frag] = None,
|
|
robots: Boolean = isGloballyCrawlable,
|
|
moreCss: Html = emptyHtml,
|
|
moreJs: Html = emptyHtml,
|
|
playing: Boolean = false,
|
|
openGraph: Option[lila.app.ui.OpenGraph] = None,
|
|
chessground: Boolean = true,
|
|
zoomable: Boolean = false,
|
|
asyncJs: Boolean = false,
|
|
csp: Option[ContentSecurityPolicy] = None,
|
|
responsive: Boolean = false,
|
|
wrapClass: String = ""
|
|
)(body: Html)(implicit ctx: Context) = frag(
|
|
doctype,
|
|
htmlTag(ctx)(
|
|
topComment,
|
|
head(
|
|
charset,
|
|
responsive option viewport,
|
|
metaCsp(csp),
|
|
if (isProd) frag(
|
|
st.headTitle(fullTitle | s"$title • lichess.org"),
|
|
!responsive option fontStylesheets
|
|
)
|
|
else st.headTitle(s"[dev] ${fullTitle | s"$title • lichess.org"}"),
|
|
if (responsive) frag(
|
|
responsiveCssTag("site"),
|
|
ctx.pref.is3d option responsiveCssTag("board-3d")
|
|
)
|
|
else frag(
|
|
responsive option cssTag("offline-fonts.css"),
|
|
currentBgCss,
|
|
cssTag("common.css"),
|
|
cssTag("board.css"),
|
|
ctx.pref.is3d option cssTag("board-3d.css"),
|
|
ctx.zoom ifTrue zoomable map { z =>
|
|
zoomStyle(z / 100f, ctx.pref.is3d)
|
|
}
|
|
),
|
|
ctx.pref.coords == 1 option cssTag("board.coords.inner.css"),
|
|
ctx.pageData.inquiry.isDefined option cssTag("inquiry.css"),
|
|
ctx.userContext.impersonatedBy.isDefined option cssTag("impersonate.css"),
|
|
isStage option responsiveCssTagNoTheme("stage"),
|
|
moreCss,
|
|
pieceSprite,
|
|
meta(content := openGraph.fold(trans.siteDescription.txt())(o => o.description), name := "description"),
|
|
link(id := "favicon", rel := "shortcut icon", href := staticUrl("images/favicon-32-white.png"), `type` := "image/x-icon"),
|
|
link(rel := "mask-icon", href := staticUrl("favicon.svg"), color := "black"),
|
|
favicons,
|
|
!robots option raw("""<meta content="noindex, nofollow" name="robots">"""),
|
|
noTranslate,
|
|
openGraph.map(_.frag),
|
|
link(href := routes.Blog.atom, `type` := "application/atom+xml", rel := "alternate", st.title := trans.blog.txt()),
|
|
ctx.transpBgImg map { img =>
|
|
raw(s"""<style type="text/css" id="bg-data">body.transp::before{background-image:url('$img');}</style>""")
|
|
},
|
|
fontPreload,
|
|
manifests
|
|
),
|
|
st.body(
|
|
cls := List(
|
|
"preload base" -> true,
|
|
ctx.currentTheme.cssClass -> true,
|
|
ctx.currentTheme3d.cssClass -> true,
|
|
(if (ctx.currentBg == "transp") "dark transp" else ctx.currentBg) -> true,
|
|
ctx.currentPieceSet3d.toString -> true,
|
|
"piece_letter" -> ctx.pref.pieceNotationIsLetter,
|
|
"zen" -> ctx.pref.isZen,
|
|
"blind_mode" -> ctx.blind,
|
|
"kid" -> ctx.kid,
|
|
"mobile" -> ctx.isMobileBrowser,
|
|
"playing fixed-scroll" -> playing
|
|
),
|
|
dataDev := (!isProd).option("true"),
|
|
dataUser := ctx.userId,
|
|
dataSoundSet := ctx.currentSoundSet.toString,
|
|
dataSocketDomain := socketDomain,
|
|
dataAssetUrl := assetBaseUrl,
|
|
dataAssetVersion := assetVersion.value,
|
|
dataNonce := ctx.nonce.map(_.value),
|
|
dataZoom := ctx.zoom.ifFalse(responsive).map(_.toString),
|
|
dataResp := responsive.option(true),
|
|
dataTheme := responsive.option(ctx.currentBg),
|
|
style := zoomable option s"--zoom:${ctx.respZoom}"
|
|
)(
|
|
blindModeForm,
|
|
ctx.pageData.inquiry map { views.html.mod.inquiry(_) },
|
|
ctx.me ifTrue ctx.userContext.impersonatedBy.isDefined map { views.html.mod.impersonate(_) },
|
|
isStage option div(id := "stage")(
|
|
"This is an empty lichess preview website for developers. ",
|
|
a(href := "https://lichess.org")("Go to lichess.org instead")
|
|
),
|
|
lila.security.EmailConfirm.cookie.get(ctx.req).map(views.html.auth.emailConfirmBanner(_)),
|
|
playing option zenToggle,
|
|
if (responsive) siteHeader.responsive(playing)
|
|
else siteHeader.old(playing),
|
|
if (responsive) div(id := "main-wrap", cls := List(
|
|
wrapClass -> wrapClass.nonEmpty,
|
|
"is2d" -> ctx.pref.is2d,
|
|
"is3d" -> ctx.pref.is3d
|
|
))(body)
|
|
else div(cls := s"content ${if (ctx.pref.is3d) "is3d" else "is2d"}")(
|
|
div(id := "site_header")(
|
|
div(id := "notifications"),
|
|
div(cls := "board_left")(
|
|
h1(
|
|
a(id := "site_title", href := routes.Lobby.home)(
|
|
if (ctx.kid) span(st.title := trans.kidMode.txt(), cls := "kiddo")("😊")
|
|
else ctx.isBot option botImage,
|
|
"lichess",
|
|
span(cls := "extension")(if (isProd) ".org" else ".dev")
|
|
)
|
|
),
|
|
menu map { sideMenu =>
|
|
div(cls := "side_menu")(sideMenu)
|
|
},
|
|
side,
|
|
chat
|
|
),
|
|
underchat map { g =>
|
|
div(cls := "under_chat")(g)
|
|
}
|
|
),
|
|
div(id := "lichess")(body)
|
|
),
|
|
ctx.me.map { me =>
|
|
div(
|
|
id := "friend_box",
|
|
dataPreload := ctx.onlineFriends.users.map(_.titleName).mkString(","),
|
|
dataPlaying := ctx.onlineFriends.playing.mkString(","),
|
|
dataPatrons := ctx.onlineFriends.patrons.mkString(","),
|
|
dataStudying := ctx.onlineFriends.studying.mkString(",")
|
|
)(
|
|
div(cls := "friend_box_title")(
|
|
strong(cls := "online")("?"),
|
|
" ",
|
|
trans.onlineFriends.frag()
|
|
),
|
|
div(cls := "content_wrap")(
|
|
div(cls := "content list"),
|
|
div(cls := List(
|
|
"nobody" -> true,
|
|
"none" -> ctx.onlineFriends.users.nonEmpty
|
|
))(
|
|
span(trans.noFriendsOnline.frag()),
|
|
a(cls := "find button", href := routes.User.opponents)(
|
|
span(cls := "is3 text", dataIcon := "h")(trans.findFriends.frag())
|
|
)
|
|
)
|
|
)
|
|
)
|
|
},
|
|
chessground option jsTag("vendor/chessground.min.js"),
|
|
ctx.requiresFingerprint option fingerprintTag,
|
|
jsAt(s"compiled/lichess.site${isProd ?? ".min"}.js", async = asyncJs),
|
|
moreJs,
|
|
embedJs(s"""lichess.quantity=${lila.i18n.JsQuantity(ctx.lang)};$timeagoLocaleScript;"""),
|
|
ctx.pageData.inquiry.isDefined option jsTag("inquiry.js", async = asyncJs)
|
|
)
|
|
)
|
|
)
|
|
|
|
object siteHeader {
|
|
|
|
private val topnavToggle = spaceless("""
|
|
<input type="checkbox" id="tn-tg" class="topnav-toggle fullscreen-toggle" aria-label="Navigation">
|
|
<label for="tn-tg" class="fullscreen-mask"></label>
|
|
<label for="tn-tg" class="hbg"><span class="hbg__in"></span></label>""")
|
|
|
|
private def reconnecting(implicit ctx: Context) =
|
|
a(id := "reconnecting", cls := "link text", dataIcon := "B")(trans.reconnecting.frag())
|
|
|
|
private def reports(implicit ctx: Context) = isGranted(_.SeeReport) option
|
|
a(cls := "link data-count", title := "Moderation", href := routes.Report.list, dataCount := reportNbOpen, dataIcon := "")
|
|
|
|
private def teamRequests(implicit ctx: Context) = ctx.teamNbRequests > 0 option
|
|
a(cls := "link data-count", href := routes.Team.requests, dataCount := ctx.teamNbRequests, dataIcon := "f", title := trans.teams.txt())
|
|
|
|
def responsive(playing: Boolean)(implicit ctx: Context) =
|
|
header(id := "top")(
|
|
div(cls := "site-title-nav")(
|
|
topnavToggle,
|
|
h1(cls := "site-title")(
|
|
a(href := "/")(
|
|
if (ctx.kid) span(title := trans.kidMode.txt(), cls := "kiddo")("😊")
|
|
else ctx.isBot option botImage,
|
|
"lichess",
|
|
span(if (isProd) ".org" else " dev")
|
|
)
|
|
),
|
|
topmenu()
|
|
),
|
|
reconnecting,
|
|
div(cls := "site-buttons")(
|
|
clinput,
|
|
reports,
|
|
teamRequests,
|
|
ctx.me map { me =>
|
|
frag(allNotifications, dasher(me))
|
|
} getOrElse { !ctx.pageData.error option anonDasher(playing) }
|
|
)
|
|
)
|
|
|
|
def old(playing: Boolean)(implicit ctx: Context) =
|
|
div(id := "top", cls := (if (ctx.pref.is3d) "is3d" else "is2d"))(
|
|
topmenu(),
|
|
div(id := "ham-plate", cls := "link", title := trans.menu.txt())(
|
|
div(id := "hamburger", dataIcon := "[")
|
|
),
|
|
ctx.me map { me =>
|
|
frag(dasher(me), allNotifications)
|
|
} getOrElse {
|
|
!ctx.pageData.error option anonDasher(playing)
|
|
},
|
|
teamRequests,
|
|
reports,
|
|
clinput,
|
|
reconnecting
|
|
)
|
|
}
|
|
}
|