2018-12-03 01:42:53 -07:00
|
|
|
package views.html.base
|
|
|
|
|
|
|
|
import play.twirl.api.Html
|
|
|
|
|
|
|
|
import lila.api.Context
|
|
|
|
import lila.app.templating.Environment._
|
2018-12-04 06:16:52 -07:00
|
|
|
import lila.app.ui.ScalatagsTemplate._
|
2019-01-16 03:33:31 -07:00
|
|
|
import lila.common.ContentSecurityPolicy
|
2018-12-10 21:04:59 -07:00
|
|
|
import scalatags.Text.tags2.{ title => titleTag }
|
2018-12-03 01:42:53 -07:00
|
|
|
|
|
|
|
import controllers.routes
|
|
|
|
|
|
|
|
object layout {
|
|
|
|
|
|
|
|
private val fontVersion = 82
|
|
|
|
|
2019-01-16 03:33:31 -07:00
|
|
|
val doctype = raw("<!doctype html>")
|
|
|
|
val topComment = raw("""<!-- Lichess is open source! See https://github.com/ornicar/lila -->""")
|
|
|
|
val charset = raw("""<meta charset="utf-8">""")
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-12-03 01:42:53 -07:00
|
|
|
private 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)
|
|
|
|
private val noTranslate = raw("""<meta name="google" content="notranslate" />""")
|
2018-12-04 21:46:17 -07:00
|
|
|
private val fontPreload = raw(s"""<link rel="preload" href="${staticUrl(s"font$fontVersion/fonts/lichess.woff")}" as="font" type="font/woff" crossorigin/>""")
|
2018-12-03 01:42:53 -07:00
|
|
|
private val wasmx = raw("""<meta http-equiv="origin-trial" data-feature="WebAssembly Threads" data-expires="2018-12-12" content="AvQS5g8cLXUfw7Vu3lmQ6B55HURS0KMuY6blwOUmAPYX3Jph8GXMSO4/jTx3el3BxG8SyJRlQTbsRRsjOHwiCAMAAABXeyJvcmlnaW4iOiJodHRwczovL2xpY2hlc3Mub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseVRocmVhZHMiLCJleHBpcnkiOjE1NDQ2MzY2MzN9">""")
|
|
|
|
private val manifests = raw(List(
|
|
|
|
"""<link rel="manifest" href="/manifest.json" />""",
|
2018-12-13 10:27:23 -07:00
|
|
|
"""<meta name="twitter:site" content="@lichess" />"""
|
2018-12-03 01:42:53 -07:00
|
|
|
).mkString)
|
|
|
|
|
|
|
|
private val favicons = raw {
|
2018-12-04 06:16:52 -07:00
|
|
|
List(256, 128, 64) map { px =>
|
2018-12-03 01:42:53 -07:00
|
|
|
s"""<link rel="icon" type="image/png" href="${staticUrl(s"favicon.$px.png")}" sizes="${px}x${px}"/>"""
|
|
|
|
} mkString
|
|
|
|
}
|
2018-12-06 04:19:01 -07:00
|
|
|
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.blindMode) 0 else 1}" /><input type="hidden" name="redirect" value="${ctx.req.path}" /><button type="submit">Accessibility: ${if (ctx.blindMode) "Disable" else "Enable"} blind mode</button></form>""")
|
2018-12-03 01:42:53 -07:00
|
|
|
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 challenges(implicit ctx: Context) = spaceless(s"""<div class="challenge_notifications">
|
|
|
|
<a id="challenge_notifications_tag" class="toggle link data-count" data-count="${ctx.nbChallenges}">
|
|
|
|
<span class="hint--bottom-left" data-hint="${trans.challenges()}"><span data-icon="U"></span></span>
|
|
|
|
</a>
|
|
|
|
<div id="challenge_app" class="dropdown"></div>
|
|
|
|
</div>""")
|
|
|
|
|
|
|
|
private def notifications(implicit ctx: Context) = spaceless(s"""<div class="site_notifications">
|
|
|
|
<a id="site_notifications_tag" class="toggle link data-count" data-count="${ctx.nbNotifications}">
|
|
|
|
<span class="hint--bottom-left" data-hint="${trans.notifications()}"><span data-icon=""></span></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 anon">
|
|
|
|
<span class="hint--bottom-left" data-hint="${trans.preferences()}"><span data-icon="%"></span></span>
|
|
|
|
</a>
|
|
|
|
<div id="dasher_app" class="dropdown" data-playing="$playing"></div>
|
|
|
|
</div>
|
|
|
|
<a href="${routes.Auth.login}?referrer=${currentPath}" class="signin button text">${trans.signIn()}</a>""")
|
|
|
|
|
2018-12-10 21:14:13 -07:00
|
|
|
private val clinputLink = a(cls := "link")(span(dataIcon := "y"))
|
|
|
|
|
2018-12-03 01:42:53 -07:00
|
|
|
private def clinput(implicit ctx: Context) =
|
2018-12-10 07:18:55 -07:00
|
|
|
div(id := "clinput")(
|
2018-12-10 21:14:13 -07:00
|
|
|
clinputLink,
|
2018-12-10 07:18:55 -07:00
|
|
|
input(spellcheck := "false", placeholder := trans.search.txt())
|
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
|
|
|
|
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 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,
|
|
|
|
baseline: Option[Html] = None,
|
|
|
|
side: Option[Html] = None,
|
|
|
|
menu: Option[Html] = None,
|
|
|
|
chat: Option[Html] = None,
|
|
|
|
underchat: Option[Html] = 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,
|
2019-01-16 03:33:31 -07:00
|
|
|
csp: Option[ContentSecurityPolicy] = None
|
2018-12-03 02:10:20 -07:00
|
|
|
)(body: Html)(implicit ctx: Context) = frag(
|
|
|
|
doctype,
|
2018-12-03 01:42:53 -07:00
|
|
|
html(st.lang := ctx.lang.language)(
|
|
|
|
topComment,
|
|
|
|
head(
|
|
|
|
charset,
|
2019-01-16 03:33:31 -07:00
|
|
|
metaCsp(csp),
|
2018-12-03 01:42:53 -07:00
|
|
|
if (isProd) frag(
|
|
|
|
wasmxEnabled() option wasmx,
|
|
|
|
titleTag(fullTitle | s"$title • lichess.org"),
|
|
|
|
fontStylesheets
|
|
|
|
)
|
|
|
|
else frag(
|
|
|
|
titleTag(s"[dev] ${fullTitle | s"$title • lichess.org"}"),
|
|
|
|
cssAt("offline/font.noto.css"),
|
|
|
|
cssAt("offline/font.roboto.mono.css")
|
|
|
|
),
|
2019-01-16 03:33:31 -07:00
|
|
|
currentBgCss,
|
2018-12-03 01:42:53 -07:00
|
|
|
cssTag("common.css"),
|
|
|
|
cssTag("board.css"),
|
|
|
|
ctx.zoom ifTrue zoomable map { z =>
|
|
|
|
zoomStyle(z / 100f, ctx.pref.is3d)
|
|
|
|
},
|
|
|
|
ctx.pref.is3d option cssTag("board-3d.css"),
|
|
|
|
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 cssTag("stage.css"),
|
|
|
|
moreCss,
|
|
|
|
link(id := "piece-sprite", href := assetUrl(s"stylesheets/piece/${ctx.currentPieceSet}.css"), `type` := "text/css", rel := "stylesheet"),
|
|
|
|
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),
|
2018-12-03 02:42:16 -07:00
|
|
|
link(href := routes.Blog.atom, `type` := "application/atom+xml", rel := "alternate", st.title := trans.blog.txt()),
|
2018-12-03 01:42:53 -07:00
|
|
|
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.blindMode,
|
|
|
|
"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.map(_.toString)
|
|
|
|
)(
|
|
|
|
blindModeForm,
|
|
|
|
div(id := "site_description")(trans.siteDescription()),
|
|
|
|
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,
|
|
|
|
div(id := "top", cls := (if (ctx.pref.is3d) "is3d" else "is2d"))(
|
|
|
|
topmenu(),
|
|
|
|
div(id := "ham-plate", cls := "link hint--bottom", dataHint := trans.menu.txt())(
|
|
|
|
div(id := "hamburger", dataIcon := "[")
|
|
|
|
),
|
|
|
|
ctx.me map { me =>
|
|
|
|
frag(dasher(me), challenges, notifications)
|
|
|
|
} getOrElse {
|
|
|
|
!ctx.pageData.error option anonDasher(playing)
|
|
|
|
},
|
|
|
|
ctx.teamNbRequests > 0 option
|
|
|
|
a(cls := "link data-count", href := routes.Team.requests, dataCount := ctx.teamNbRequests)(
|
|
|
|
span(cls := "hint--bottom-left", dataHint := trans.teams.txt())(span(dataIcon := "f"))
|
|
|
|
),
|
|
|
|
isGranted(_.SeeReport) option
|
|
|
|
a(cls := "link text data-count", href := routes.Report.list, dataCount := reportNbOpen, dataIcon := ""),
|
|
|
|
clinput,
|
|
|
|
a(id := "reconnecting", cls := "link text", dataIcon := "B")(trans.reconnecting())
|
|
|
|
),
|
|
|
|
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")
|
|
|
|
)
|
|
|
|
),
|
|
|
|
baseline,
|
|
|
|
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(",")
|
|
|
|
)(
|
2018-12-11 12:10:19 -07:00
|
|
|
div(cls := "friend_box_title")(
|
2018-12-03 01:42:53 -07:00
|
|
|
strong(cls := "online")(" "),
|
|
|
|
" ",
|
|
|
|
trans.onlineFriends()
|
|
|
|
),
|
|
|
|
div(cls := "content_wrap")(
|
2018-12-13 12:50:21 -07:00
|
|
|
div(cls := "content list"),
|
|
|
|
div(cls := List(
|
|
|
|
"nobody" -> true,
|
|
|
|
"none" -> ctx.onlineFriends.users.nonEmpty
|
|
|
|
))(
|
|
|
|
span(trans.noFriendsOnline()),
|
|
|
|
a(cls := "find button", href := routes.User.opponents)(
|
|
|
|
span(cls := "is3 text", dataIcon := "h")(trans.findFriends())
|
2018-12-03 01:42:53 -07:00
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
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)
|
|
|
|
)
|
|
|
|
)
|
2018-12-03 02:10:20 -07:00
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
}
|