2018-12-03 01:42:53 -07:00
|
|
|
package views.html.base
|
|
|
|
|
2019-12-07 13:45:01 -07:00
|
|
|
import play.api.i18n.Lang
|
2019-12-08 11:12:00 -07:00
|
|
|
import play.api.libs.json.Json
|
2019-09-08 09:07:49 -06:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
import lila.api.{ AnnounceStore, Context }
|
2018-12-03 01:42:53 -07:00
|
|
|
import lila.app.templating.Environment._
|
2018-12-04 06:16:52 -07:00
|
|
|
import lila.app.ui.ScalatagsTemplate._
|
2019-12-07 13:45:01 -07:00
|
|
|
import lila.common.ContentSecurityPolicy
|
2019-12-08 11:12:00 -07:00
|
|
|
import lila.common.String.html.safeJsonValue
|
2018-12-03 01:42:53 -07:00
|
|
|
|
|
|
|
import controllers.routes
|
|
|
|
|
|
|
|
object layout {
|
|
|
|
|
2019-01-16 19:19:41 -07:00
|
|
|
object bits {
|
2019-12-19 21:04:22 -07:00
|
|
|
val doctype = raw("<!DOCTYPE html>")
|
2019-12-01 23:25:40 -07:00
|
|
|
def htmlTag(implicit lang: Lang) = html(st.lang := lang.code)
|
2019-12-13 07:30:20 -07:00
|
|
|
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">"""
|
|
|
|
)
|
2019-09-02 04:31:50 -06:00
|
|
|
def metaCsp(csp: ContentSecurityPolicy): Frag = raw {
|
|
|
|
s"""<meta http-equiv="Content-Security-Policy" content="$csp">"""
|
|
|
|
}
|
2019-12-13 07:30:20 -07:00
|
|
|
def metaCsp(csp: Option[ContentSecurityPolicy])(implicit ctx: Context): Frag =
|
|
|
|
metaCsp(csp getOrElse defaultCsp)
|
2019-10-22 15:47:53 -06:00
|
|
|
def metaThemeColor(implicit ctx: Context): Frag = raw {
|
|
|
|
s"""<meta name="theme-color" content="${ctx.pref.themeColor}">"""
|
|
|
|
}
|
2019-04-08 03:54:45 -06:00
|
|
|
def pieceSprite(implicit ctx: Context): Frag = pieceSprite(ctx.currentPieceSet)
|
|
|
|
def pieceSprite(ps: lila.pref.PieceSet): Frag =
|
2019-12-13 07:30:20 -07:00
|
|
|
link(
|
|
|
|
id := "piece-sprite",
|
|
|
|
href := assetUrl(s"piece-css/$ps.css"),
|
|
|
|
tpe := "text/css",
|
|
|
|
rel := "stylesheet"
|
|
|
|
)
|
2019-01-16 03:33:31 -07:00
|
|
|
}
|
2019-01-16 19:19:41 -07:00
|
|
|
import bits._
|
2019-01-16 03:33:31 -07:00
|
|
|
|
2019-12-02 02:39:16 -07:00
|
|
|
private val noTranslate = raw("""<meta name="google" content="notranslate">""")
|
2019-04-26 02:35:12 -06:00
|
|
|
private def fontPreload(implicit ctx: Context) = raw {
|
2019-12-02 02:39:16 -07:00
|
|
|
s"""<link rel="preload" href="${assetUrl(s"font/lichess.woff2")}" as="font" type="font/woff2" crossorigin>""" +
|
2019-04-26 02:35:12 -06:00
|
|
|
!ctx.pref.pieceNotationIsLetter ??
|
2019-12-13 07:30:20 -07:00
|
|
|
s"""<link rel="preload" href="${assetUrl(s"font/lichess.chess.woff2")}" as="font" type="font/woff2" crossorigin>"""
|
2019-04-26 02:35:12 -06:00
|
|
|
}
|
2019-12-13 07:30:20 -07:00
|
|
|
private val manifests = raw(
|
|
|
|
"""<link rel="manifest" href="/manifest.json"><meta name="twitter:site" content="@lichess">"""
|
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
|
2019-12-02 02:39:16 -07:00
|
|
|
private val jsLicense = raw("""<link rel="jslicense" href="/source">""")
|
2019-04-26 07:35:32 -06:00
|
|
|
|
2018-12-03 01:42:53 -07:00
|
|
|
private val favicons = raw {
|
2019-12-02 02:54:06 -07:00
|
|
|
List(512, 256, 192, 128, 64) map { px =>
|
2019-12-02 02:40:09 -07:00
|
|
|
s"""<link rel="icon" type="image/png" href="${staticUrl(s"logo/lichess-favicon-$px.png")}" sizes="${px}x${px}">"""
|
2019-12-13 07:30:20 -07:00
|
|
|
} mkString ("", "", s"""<link id="favicon" rel="icon" type="image/png" href="${staticUrl(
|
|
|
|
"logo/lichess-favicon-32.png"
|
|
|
|
)}" sizes="32x32">""")
|
2018-12-03 01:42:53 -07:00
|
|
|
}
|
2019-12-13 07:30:20 -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.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>""")
|
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>""")
|
2019-12-13 07:30:20 -07:00
|
|
|
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>"""
|
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
private def allNotifications(implicit ctx: Context) =
|
|
|
|
spaceless(s"""<div>
|
2019-04-17 03:29:30 -06:00
|
|
|
<a id="challenge-toggle" class="toggle link">
|
2019-12-13 07:30:20 -07:00
|
|
|
<span title="${trans
|
|
|
|
.challenges()
|
|
|
|
.render}" class="data-count" data-count="${ctx.nbChallenges}" data-icon="U"></span>
|
2018-12-03 01:42:53 -07:00
|
|
|
</a>
|
2019-04-17 03:29:30 -06:00
|
|
|
<div id="challenge-app" class="dropdown"></div>
|
2019-01-19 17:43:18 -07:00
|
|
|
</div>
|
2019-04-17 03:29:30 -06:00
|
|
|
<div>
|
|
|
|
<a id="notify-toggle" class="toggle link">
|
2019-12-13 07:30:20 -07:00
|
|
|
<span title="${trans
|
|
|
|
.notifications()
|
|
|
|
.render}" class="data-count" data-count="${ctx.nbNotifications}" data-icon=""></span>
|
2018-12-03 01:42:53 -07:00
|
|
|
</a>
|
2019-04-17 03:29:30 -06:00
|
|
|
<div id="notify-app" class="dropdown"></div>
|
2018-12-03 01:42:53 -07:00
|
|
|
</div>""")
|
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
private def anonDasher(playing: Boolean)(implicit ctx: Context) =
|
|
|
|
spaceless(s"""<div class="dasher">
|
2019-02-11 17:53:15 -07:00
|
|
|
<a class="toggle link anon">
|
2020-02-09 21:40:00 -07:00
|
|
|
<span title="${trans.preferences.preferences().render}" data-icon="%"></span>
|
2018-12-03 01:42:53 -07:00
|
|
|
</a>
|
|
|
|
<div id="dasher_app" class="dropdown" data-playing="$playing"></div>
|
|
|
|
</div>
|
2019-12-13 07:30:20 -07:00
|
|
|
<a href="${routes.Auth.login}?referrer=${ctx.req.path}" class="signin button button-empty">${trans
|
|
|
|
.signIn()
|
|
|
|
.render}</a>""")
|
2018-12-03 01:42:53 -07:00
|
|
|
|
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,
|
2019-10-12 08:24:00 -06:00
|
|
|
input(
|
|
|
|
spellcheck := "false",
|
2019-10-24 03:27:56 -06:00
|
|
|
autocomplete := ctx.blind.toString,
|
2020-02-10 15:43:11 -07:00
|
|
|
aria.label := trans.search.search.txt(),
|
|
|
|
placeholder := trans.search.search.txt()
|
2019-10-12 08:24:00 -06:00
|
|
|
)
|
2018-12-10 07:18:55 -07:00
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
|
2019-12-13 07:30:20 -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;vertical-align:text-top"
|
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
private val spaceRegex = """\s{2,}+""".r
|
2018-12-03 01:42:53 -07:00
|
|
|
private def spaceless(html: String) = raw(spaceRegex.replaceAllIn(html.replace("\\n", ""), ""))
|
|
|
|
|
2020-01-03 13:47:38 -07:00
|
|
|
private val dataVapid = attr("data-vapid")
|
|
|
|
private val dataUser = attr("data-user")
|
|
|
|
private val dataSoundSet = attr("data-sound-set")
|
|
|
|
private val dataSocketDomains = attr("data-socket-domains")
|
|
|
|
private val dataPreload = attr("data-preload")
|
|
|
|
private val dataNonce = attr("data-nonce")
|
|
|
|
private val dataAnnounce = attr("data-announce")
|
2018-12-03 01:42:53 -07:00
|
|
|
|
|
|
|
def apply(
|
2019-12-13 07:30:20 -07:00
|
|
|
title: String,
|
|
|
|
fullTitle: Option[String] = None,
|
|
|
|
robots: Boolean = isGloballyCrawlable,
|
|
|
|
moreCss: Frag = emptyFrag,
|
|
|
|
moreJs: Frag = emptyFrag,
|
|
|
|
playing: Boolean = false,
|
|
|
|
openGraph: Option[lila.app.ui.OpenGraph] = None,
|
|
|
|
chessground: Boolean = true,
|
|
|
|
zoomable: Boolean = false,
|
|
|
|
deferJs: Boolean = false,
|
|
|
|
csp: Option[ContentSecurityPolicy] = None,
|
|
|
|
wrapClass: String = ""
|
2019-12-04 16:39:16 -07:00
|
|
|
)(body: Frag)(implicit ctx: Context): Frag = frag(
|
2018-12-03 02:10:20 -07:00
|
|
|
doctype,
|
2019-04-15 04:07:12 -06:00
|
|
|
htmlTag(ctx.lang)(
|
2018-12-03 01:42:53 -07:00
|
|
|
topComment,
|
|
|
|
head(
|
|
|
|
charset,
|
2019-04-11 04:05:52 -06:00
|
|
|
viewport,
|
2019-01-16 03:33:31 -07:00
|
|
|
metaCsp(csp),
|
2019-10-22 15:47:53 -06:00
|
|
|
metaThemeColor,
|
2019-11-17 11:55:41 -07:00
|
|
|
st.headTitle {
|
|
|
|
if (ctx.blind) "lichess"
|
|
|
|
else if (isProd && !isStage) fullTitle | s"$title • lichess.org"
|
|
|
|
else s"[dev] ${fullTitle | s"$title • lichess.dev"}"
|
|
|
|
},
|
2019-04-21 08:33:50 -06:00
|
|
|
cssTag("site"),
|
|
|
|
ctx.pref.is3d option cssTag("board-3d"),
|
|
|
|
ctx.pageData.inquiry.isDefined option cssTagNoTheme("mod.inquiry"),
|
|
|
|
ctx.userContext.impersonatedBy.isDefined option cssTagNoTheme("mod.impersonate"),
|
2019-05-02 04:19:53 -06:00
|
|
|
ctx.blind option cssTagNoTheme("blind"),
|
2018-12-03 01:42:53 -07:00
|
|
|
moreCss,
|
2019-01-16 19:19:41 -07:00
|
|
|
pieceSprite,
|
2019-12-13 07:30:20 -07:00
|
|
|
meta(
|
|
|
|
content := openGraph.fold(trans.siteDescription.txt())(o => o.description),
|
|
|
|
name := "description"
|
|
|
|
),
|
2019-12-02 03:02:19 -07:00
|
|
|
link(rel := "mask-icon", href := staticUrl("logo/lichess.svg"), color := "black"),
|
2018-12-03 01:42:53 -07:00
|
|
|
favicons,
|
|
|
|
!robots option raw("""<meta content="noindex, nofollow" name="robots">"""),
|
|
|
|
noTranslate,
|
2019-04-09 03:46:13 -06:00
|
|
|
openGraph.map(_.frags),
|
2019-12-13 07:30:20 -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 =>
|
2019-12-13 07:30:20 -07:00
|
|
|
raw(
|
|
|
|
s"""<style type="text/css" id="bg-data">body.transp::before{background-image:url('$img');}</style>"""
|
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
},
|
|
|
|
fontPreload,
|
2019-04-26 07:35:32 -06:00
|
|
|
manifests,
|
|
|
|
jsLicense
|
2018-12-03 01:42:53 -07:00
|
|
|
),
|
|
|
|
st.body(
|
|
|
|
cls := List(
|
2019-05-04 03:17:58 -06:00
|
|
|
s"${ctx.currentBg} ${ctx.currentTheme.cssClass} ${ctx.currentTheme3d.cssClass} ${ctx.currentPieceSet3d.toString} coords-${ctx.pref.coordsClass}" -> true,
|
2019-12-13 07:30:20 -07:00
|
|
|
"piece-letter" -> ctx.pref.pieceNotationIsLetter,
|
|
|
|
"zen" -> ctx.pref.isZen,
|
|
|
|
"blind-mode" -> ctx.blind,
|
|
|
|
"kid" -> ctx.kid,
|
|
|
|
"mobile" -> ctx.isMobileBrowser,
|
|
|
|
"playing fixed-scroll" -> playing
|
2018-12-03 01:42:53 -07:00
|
|
|
),
|
|
|
|
dataDev := (!isProd).option("true"),
|
2019-09-02 04:29:03 -06:00
|
|
|
dataVapid := vapidPublicKey,
|
2018-12-03 01:42:53 -07:00
|
|
|
dataUser := ctx.userId,
|
|
|
|
dataSoundSet := ctx.currentSoundSet.toString,
|
2020-01-03 13:47:38 -07:00
|
|
|
dataSocketDomains := socketDomains.mkString(","),
|
2018-12-03 01:42:53 -07:00
|
|
|
dataAssetUrl := assetBaseUrl,
|
|
|
|
dataAssetVersion := assetVersion.value,
|
2019-04-20 20:35:05 -06:00
|
|
|
dataNonce := ctx.nonce.ifTrue(sameAssetDomain).map(_.value),
|
2019-04-11 04:05:52 -06:00
|
|
|
dataTheme := ctx.currentBg,
|
2019-10-12 06:03:36 -06:00
|
|
|
dataAnnounce := AnnounceStore.get.map(a => safeJsonValue(a.json)),
|
2019-05-04 18:56:12 -06:00
|
|
|
style := zoomable option s"--zoom:${ctx.zoom}"
|
2018-12-03 01:42:53 -07:00
|
|
|
)(
|
2019-12-13 07:30:20 -07:00
|
|
|
blindModeForm,
|
|
|
|
ctx.pageData.inquiry map { views.html.mod.inquiry(_) },
|
|
|
|
ctx.me ifTrue ctx.userContext.impersonatedBy.isDefined map { views.html.mod.impersonate(_) },
|
|
|
|
isStage option views.html.base.bits.stage,
|
|
|
|
lila.security.EmailConfirm.cookie.get(ctx.req).map(views.html.auth.bits.checkYourEmailBanner(_)),
|
|
|
|
playing option zenToggle,
|
|
|
|
siteHeader(playing),
|
|
|
|
div(
|
|
|
|
id := "main-wrap",
|
|
|
|
cls := List(
|
2019-02-24 00:02:59 -07:00
|
|
|
wrapClass -> wrapClass.nonEmpty,
|
2019-12-13 07:30:20 -07:00
|
|
|
"is2d" -> ctx.pref.is2d,
|
|
|
|
"is3d" -> ctx.pref.is3d
|
|
|
|
)
|
|
|
|
)(body),
|
2020-03-25 12:36:14 -06:00
|
|
|
ctx.isAuth option div(
|
2019-12-13 07:30:20 -07:00
|
|
|
id := "friend_box",
|
2020-03-25 12:36:14 -06:00
|
|
|
dataPreload := safeJsonValue(Json.obj("i18n" -> i18nJsObject(i18nKeys)))
|
2019-12-13 07:30:20 -07:00
|
|
|
)(
|
2020-03-27 11:03:13 -06:00
|
|
|
div(cls := "friend_box_title")(trans.nbFriendsOnline.plural(0, iconTag("S"))),
|
2020-03-25 13:39:23 -06:00
|
|
|
div(cls := "content_wrap none")(
|
2020-03-25 12:36:14 -06:00
|
|
|
div(cls := "content list")
|
2019-12-13 07:30:20 -07:00
|
|
|
)
|
|
|
|
),
|
|
|
|
a(id := "reconnecting", cls := "link text", dataIcon := "B")(trans.reconnecting()),
|
|
|
|
chessground option jsTag("vendor/chessground.min.js"),
|
|
|
|
ctx.requiresFingerprint option fingerprintTag,
|
|
|
|
if (isProd)
|
|
|
|
jsAt(s"compiled/lichess.site.min.js", defer = deferJs)
|
|
|
|
else
|
|
|
|
frag(
|
2019-04-21 00:46:17 -06:00
|
|
|
jsAt(s"compiled/lichess.deps.js", defer = deferJs),
|
|
|
|
jsAt(s"compiled/lichess.site.js", defer = deferJs)
|
2019-04-20 09:37:38 -06:00
|
|
|
),
|
2019-12-13 07:30:20 -07:00
|
|
|
moreJs,
|
|
|
|
embedJsUnsafe(s"""lichess.quantity=${lila.i18n.JsQuantity(ctx.lang)};$timeagoLocaleScript"""),
|
|
|
|
ctx.pageData.inquiry.isDefined option jsTag("inquiry.js", defer = deferJs)
|
|
|
|
)
|
2018-12-03 01:42:53 -07:00
|
|
|
)
|
2018-12-03 02:10:20 -07:00
|
|
|
)
|
2019-02-08 04:13:50 -07:00
|
|
|
|
|
|
|
object siteHeader {
|
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
private val topnavToggle = spaceless(
|
|
|
|
"""
|
2019-02-13 09:54:24 -07:00
|
|
|
<input type="checkbox" id="tn-tg" class="topnav-toggle fullscreen-toggle" aria-label="Navigation">
|
|
|
|
<label for="tn-tg" class="fullscreen-mask"></label>
|
2019-12-13 07:30:20 -07:00
|
|
|
<label for="tn-tg" class="hbg"><span class="hbg__in"></span></label>"""
|
|
|
|
)
|
2019-02-08 04:13:50 -07:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
private def reports(implicit ctx: Context) =
|
|
|
|
isGranted(_.SeeReport) option
|
|
|
|
a(
|
|
|
|
cls := "link data-count link-center",
|
|
|
|
title := "Moderation",
|
|
|
|
href := routes.Report.list,
|
|
|
|
dataCount := blockingReportNbOpen,
|
|
|
|
dataIcon := ""
|
|
|
|
)
|
2019-02-08 04:13:50 -07:00
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
private def teamRequests(implicit ctx: Context) =
|
|
|
|
ctx.teamNbRequests > 0 option
|
|
|
|
a(
|
|
|
|
cls := "link data-count link-center",
|
|
|
|
href := routes.Team.requests,
|
|
|
|
dataCount := ctx.teamNbRequests,
|
|
|
|
dataIcon := "f",
|
2020-02-10 09:25:44 -07:00
|
|
|
title := trans.team.teams.txt()
|
2019-12-13 07:30:20 -07:00
|
|
|
)
|
2019-02-08 04:13:50 -07:00
|
|
|
|
2019-04-11 04:05:52 -06:00
|
|
|
def apply(playing: Boolean)(implicit ctx: Context) =
|
2019-02-09 02:07:12 -07:00
|
|
|
header(id := "top")(
|
2019-02-08 04:13:50 -07:00
|
|
|
div(cls := "site-title-nav")(
|
|
|
|
topnavToggle,
|
|
|
|
h1(cls := "site-title")(
|
2019-05-03 02:39:07 -06:00
|
|
|
if (ctx.kid) span(title := trans.kidMode.txt(), cls := "kiddo")(":)")
|
|
|
|
else ctx.isBot option botImage,
|
2019-02-08 04:13:50 -07:00
|
|
|
a(href := "/")(
|
|
|
|
"lichess",
|
2019-03-07 03:29:21 -07:00
|
|
|
span(if (isProd && !isStage) ".org" else ".dev")
|
2019-02-08 04:13:50 -07:00
|
|
|
)
|
|
|
|
),
|
2019-05-02 04:19:53 -06:00
|
|
|
ctx.blind option h2("Navigation"),
|
2019-04-07 20:21:04 -06:00
|
|
|
topnav()
|
2019-02-08 04:13:50 -07:00
|
|
|
),
|
|
|
|
div(cls := "site-buttons")(
|
|
|
|
clinput,
|
|
|
|
reports,
|
|
|
|
teamRequests,
|
|
|
|
ctx.me map { me =>
|
|
|
|
frag(allNotifications, dasher(me))
|
|
|
|
} getOrElse { !ctx.pageData.error option anonDasher(playing) }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2020-02-12 09:46:59 -07:00
|
|
|
|
|
|
|
private val i18nKeys = List(trans.nbFriendsOnline.key)
|
2018-12-03 01:42:53 -07:00
|
|
|
}
|