Merge branch 'v2'

* v2: (1324 commits)
  email-confirm head bar z-index
  fix email change tokens after 5c94ebc99a
  fix public tournament chat not available
  tweak coord position
  render analysis gauge after the board so it goes over the rank coords
  refactor coords
  include all coords CSS in board pages
  implement menuHover in board editor
  board editor code tweaks
  implement menuHover in puzzles & analysis
  implement menuHover in round
  specific lichess topnav hoverIntent implementation
  fix insight max board size
  fix top bar on long following pages
  don't reply to lichess messages, nobody reads that
  send a message when the max follow limit is reached
  fix max-follow bug
  responsive coach editor
  fix account pages height on mobile
  fix round moves list index width
  ...
pull/5042/head
Thibault Duplessis 2019-05-05 06:26:47 +07:00
commit 0725765d20
1745 changed files with 52054 additions and 40105 deletions

2
.gitignore vendored
View File

@ -9,6 +9,7 @@ public/vendor/stockfish.wasm
public/vendor/stockfish-mv.wasm
public/vendor/stockfish.pexe
public/vendor/stockfish.js
public/css/
target
bin/.translate_version
data/
@ -17,6 +18,7 @@ node_modules/
local/
ui/*/npm-debug.log
hs_*.log
yarn-error.log
RUNNING_PID

View File

@ -36,7 +36,7 @@ Exceptions (free)
Files | Author(s) | License
--- | --- | ---
public/font70 | [Dave Gandy](http://fontawesome.io/), [GitHub](https://github.com/primer/octicons), [Webalys](http://www.webalys.com/), [Zurb](http://zurb.com/playground/foundation-icon-fonts-3), [Daniel Bruce](http://www.entypo.com/), [Shapemade](http://steadysets.com/), [Sergey Shmidt](http://designmodo.com/linecons-free/) and the lichess authors | [OFL](http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), [MIT](https://github.com/primer/octicons/blob/master/LICENSE), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/), AGPLv3+
ChessSansPiratf in public/fonts | the [pgn4web](http://pgn4web.casaschi.net/home.html) authors | [GPLv2+](https://www.gnu.org/licenses/gpl-2.0.txt)
ChessSansPiratf in public/font | the [pgn4web](http://pgn4web.casaschi.net/home.html) authors | [GPLv2+](https://www.gnu.org/licenses/gpl-2.0.txt)
public/images/staunton | [James Clarke](https://github.com/clarkerubber/Staunton-Pieces) | [MIT](https://github.com/clarkerubber/Staunton-Pieces/blob/master/LICENSE)
public/images/staunton/piece/CubesAndPi | CubesAndPi | AGPLv3+
public/images/trophy | [James Clarke](https://github.com/clarkerubber/Staunton-Pieces/tree/master/Trophies) | [MIT](https://github.com/clarkerubber/Staunton-Pieces/blob/master/LICENSE)

View File

@ -2,7 +2,6 @@ package lila.app
package actor
import akka.actor._
import play.twirl.api.Html
import lila.game.Pov
import views.{ html => V }
@ -12,10 +11,10 @@ private[app] final class Renderer extends Actor {
def receive = {
case lila.tv.actorApi.RenderFeaturedJs(game) =>
sender ! V.game.bits.featuredJs(Pov first game).body
sender ! V.game.bits.featuredJs(Pov first game).render
case lila.tournament.actorApi.TournamentTable(tours) =>
sender ! V.tournament.enterable(tours).render
sender ! V.tournament.bits.enterable(tours).render
case lila.simul.actorApi.SimulTable(simuls) =>
sender ! V.simul.bits.allCreated(simuls).render

View File

@ -68,16 +68,16 @@ object Analyse extends LilaController {
}
}
def embed(gameId: String, color: String) = Open { implicit ctx =>
def embed(gameId: String, color: String) = Action.async { implicit req =>
GameRepo.gameWithInitialFen(gameId) flatMap {
case Some((game, initialFen)) =>
val pov = Pov(game, chess.Color(color == "white"))
Env.api.roundApi.review(pov, lila.api.Mobile.Api.currentVersion,
Env.api.roundApi.embed(pov, lila.api.Mobile.Api.currentVersion,
initialFenO = initialFen.some,
withFlags = WithFlags(opening = true)) map { data =>
Ok(html.analyse.embed(pov, data))
}
case _ => fuccess(NotFound(html.analyse.embed.notFound()))
case _ => fuccess(NotFound(html.analyse.embed.notFound))
}
}

View File

@ -43,7 +43,7 @@ object Api extends LilaController {
}
def index = Action {
Ok(views.html.site.api())
Ok(views.html.site.bits.api)
}
def user(name: String) = CookieBasedApiRequest { ctx =>

View File

@ -42,13 +42,12 @@ object Auth extends LilaController {
}
def authenticateUser(u: UserModel, result: Option[String => Result] = None)(implicit ctx: Context): Fu[Result] = {
implicit val req = ctx.req
if (u.ipBan) fuccess(Redirect(routes.Lobby.home))
else api.saveAuthentication(u.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = fuccess {
val redirectTo = get("referrer").filter(goodReferrer) orElse
req.session.get(api.AccessUri) getOrElse
ctxReq.session.get(api.AccessUri) getOrElse
routes.Lobby.home.url
result.fold(Redirect(redirectTo))(_(redirectTo))
},
@ -118,21 +117,19 @@ object Auth extends LilaController {
}
def logout = Open { implicit ctx =>
implicit val req = ctx.req
req.session get "sessionId" foreach lila.security.Store.delete
ctxReq.session get "sessionId" foreach lila.security.Store.delete
negotiate(
html = Redirect(routes.Main.mobile).fuccess,
html = Redirect(routes.Auth.login).fuccess,
api = _ => Ok(Json.obj("ok" -> true)).fuccess
) map (_ withCookies LilaCookie.newSession)
}
// mobile app BC logout with GET
def logoutGet = Open { implicit ctx =>
implicit val req = ctx.req
negotiate(
html = notFound,
api = _ => {
req.session get "sessionId" foreach lila.security.Store.delete
ctxReq.session get "sessionId" foreach lila.security.Store.delete
Ok(Json.obj("ok" -> true)).withCookies(LilaCookie.newSession).fuccess
}
)
@ -248,7 +245,7 @@ object Auth extends LilaController {
private def welcome(user: UserModel, email: EmailAddress)(implicit ctx: Context) = {
garbageCollect(user, email)
env.welcomeEmail(user, email)
env.automaticEmail.welcome(user, email)
}
private def garbageCollect(user: UserModel, email: EmailAddress)(implicit ctx: Context) =
@ -317,7 +314,6 @@ object Auth extends LilaController {
}
private def redirectNewUser(user: UserModel)(implicit ctx: Context) = {
implicit val req = ctx.req
api.saveAuthentication(user.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = Redirect(routes.User.show(user.username)).fuccess,
@ -344,7 +340,7 @@ object Auth extends LilaController {
def passwordReset = Open { implicit ctx =>
forms.passwordResetWithCaptcha map {
case (form, captcha) => Ok(html.auth.passwordReset(form, captcha))
case (form, captcha) => Ok(html.auth.bits.passwordReset(form, captcha))
}
}
@ -352,7 +348,7 @@ object Auth extends LilaController {
implicit val req = ctx.body
forms.passwordReset.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
BadRequest(html.auth.passwordReset(err, captcha, false.some))
BadRequest(html.auth.bits.passwordReset(err, captcha, false.some))
},
data => {
UserRepo.enabledWithEmail(data.realEmail.normalize) flatMap {
@ -363,7 +359,7 @@ object Auth extends LilaController {
case _ => {
lila.mon.user.auth.passwordResetRequest("no_email")()
forms.passwordResetWithCaptcha map {
case (form, captcha) => BadRequest(html.auth.passwordReset(form, captcha, false.some))
case (form, captcha) => BadRequest(html.auth.bits.passwordReset(form, captcha, false.some))
}
}
}
@ -373,7 +369,7 @@ object Auth extends LilaController {
def passwordResetSent(email: String) = Open { implicit ctx =>
fuccess {
Ok(html.auth.passwordResetSent(email))
Ok(html.auth.bits.passwordResetSent(email))
}
}
@ -386,7 +382,7 @@ object Auth extends LilaController {
case Some(user) => {
authLog(user.username, "Reset password")
lila.mon.user.auth.passwordResetConfirm("token_ok")()
fuccess(html.auth.passwordResetConfirm(user, token, forms.passwdReset, none))
fuccess(html.auth.bits.passwordResetConfirm(user, token, forms.passwdReset, none))
}
}
}
@ -400,7 +396,7 @@ object Auth extends LilaController {
case Some(user) =>
implicit val req = ctx.body
FormFuResult(forms.passwdReset) { err =>
fuccess(html.auth.passwordResetConfirm(user, token, err, false.some))
fuccess(html.auth.bits.passwordResetConfirm(user, token, err, false.some))
} { data =>
HasherRateLimit(user.username, ctx.req) { _ =>
Env.user.authenticator.setPassword(user.id, ClearPassword(data.newPasswd1)) >>

View File

@ -49,7 +49,7 @@ object Blog extends LilaController {
blogApi context req flatMap { implicit prismic =>
blogApi.recent(prismic.api, none, 1, lila.common.MaxPerPage(50)) map {
_ ?? { docs =>
Ok(views.xml.blog.atom(docs)) as XML
Ok(views.html.blog.atom(docs)) as XML
}
}
}

View File

@ -41,13 +41,16 @@ object Challenge extends LilaController {
else none
val json = env.jsonView.show(c, version, direction)
negotiate(
html = fuccess {
if (mine) error match {
case Some(e) => BadRequest(html.challenge.mine.apply(c, json, e.some))
case None => Ok(html.challenge.mine.apply(c, json, none))
html =
if (mine) fuccess {
error match {
case Some(e) => BadRequest(html.challenge.mine(c, json, e.some))
case None => Ok(html.challenge.mine(c, json, none))
}
}
else Ok(html.challenge.theirs.apply(c, json))
},
else (c.challengerUserId ?? UserRepo.named) map { user =>
Ok(html.challenge.theirs(c, json, user))
},
api = _ => Ok(json).fuccess
) flatMap withChallengeAnonCookie(mine && c.challengerIsAnon, c, true)
}
@ -90,7 +93,6 @@ object Challenge extends LilaController {
cond ?? {
GameRepo.game(c.id).map {
_ map { game =>
implicit val req = ctx.req
LilaCookie.cookie(
AnonCookie.name,
game.player(if (owner) c.finalColor else !c.finalColor).id,

View File

@ -30,8 +30,7 @@ object Coordinate extends LilaController {
err => fuccess(BadRequest),
value => Env.pref.api.setPref(
me,
(p: lila.pref.Pref) => p.copy(coordColor = value),
notifyChange = false
(p: lila.pref.Pref) => p.copy(coordColor = value)
) inject Ok(())
)
}

View File

@ -70,8 +70,7 @@ object Dasher extends LilaController {
"image" -> ctx.pref.bgImgOrDefault
),
"board" -> Json.obj(
"is3d" -> ctx.pref.is3d,
"zoom" -> ctx.zoom
"is3d" -> ctx.pref.is3d
),
"theme" -> Json.obj(
"d2" -> Json.obj(

View File

@ -3,7 +3,6 @@ package controllers
import chess.format.Forsyth
import chess.Situation
import play.api.libs.json._
import play.twirl.api.Html
import lila.app._
import lila.game.GameRepo

View File

@ -7,12 +7,6 @@ object Event extends LilaController {
private def api = Env.event.api
def index = Open { implicit ctx =>
api.recentEnabled.map { events =>
Ok(html.event.index(events))
}
}
def show(id: String) = Open { implicit ctx =>
OptionOk(api oneEnabled id) { event =>
html.event.show(event)

View File

@ -46,7 +46,7 @@ object ForumTopic extends LilaController with ForumController {
case (categ, topic, posts) => for {
unsub <- ctx.userId ?? Env.timeline.status(s"forum:${topic.id}")
canWrite <- isGrantedWrite(categSlug)
form <- (!posts.hasNextPage && canWrite && topic.open) ?? forms.postWithCaptcha.map(_.some)
form <- (!posts.hasNextPage && canWrite && topic.open && !topic.isOld) ?? forms.postWithCaptcha.map(_.some)
canModCateg <- isGrantedMod(categ.slug)
_ <- Env.user.lightUserApi preloadMany posts.currentPageResults.flatMap(_.userId)
} yield html.forum.topic.show(categ, topic, posts, form, unsub, canModCateg = canModCateg)

View File

@ -6,8 +6,7 @@ import play.api.http._
import play.api.libs.json.{ Json, JsObject, JsArray, JsString, Writes }
import play.api.mvc._
import play.api.mvc.BodyParsers.parse
import play.twirl.api.Html
import scalatags.Text.{ TypedTag, Frag }
import scalatags.Text.Frag
import lila.api.{ PageData, Context, HeaderContext, BodyContext }
import lila.app._
@ -28,22 +27,11 @@ private[controllers] trait LilaController
protected implicit val LilaResultZero = Zero.instance[Result](Results.NotFound)
protected implicit val LilaHtmlMonoid = lila.app.templating.Environment.LilaHtmlMonoid
protected implicit final class LilaPimpedResult(result: Result) {
def fuccess = scala.concurrent.Future successful result
}
protected implicit def LilaHtmlToResult(content: Html): Result = Ok(content)
protected implicit def contentTypeOfFrag(implicit codec: Codec): ContentTypeOf[Frag] =
ContentTypeOf[Frag](Some(ContentTypes.HTML))
protected implicit def writeableOfFrag(implicit codec: Codec): Writeable[Frag] =
Writeable(frag => codec.encode(frag.render))
protected implicit def LilaScalatagsToHtml(tags: scalatags.Text.TypedTag[String]): Html = Html(tags.render)
protected implicit def LilaFragToResult(content: Frag): Result = Ok(content)
protected implicit def LilaFragToResult(frag: Frag): Result = Ok(frag)
protected implicit def makeApiVersion(v: Int) = ApiVersion(v)
@ -56,7 +44,9 @@ private[controllers] trait LilaController
api = _ => fuccess(jsonOkResult)
)
implicit def lang(implicit ctx: Context) = ctx.lang
implicit def ctxLang(implicit ctx: Context) = ctx.lang
implicit def ctxReq(implicit ctx: Context) = ctx.req
implicit def reqConfig(implicit req: RequestHeader) = ui.EmbedConfig(req)
protected def NoCache(res: Result): Result = res.withHeaders(
CACHE_CONTROL -> "no-cache, no-store, must-revalidate", EXPIRES -> "0"
@ -213,7 +203,7 @@ private[controllers] trait LilaController
protected def NoTor(res: => Fu[Result])(implicit ctx: Context) =
if (Env.security.tor isExitNode HTTPRequest.lastRemoteAddress(ctx.req))
Unauthorized(views.html.auth.tor()).fuccess
Unauthorized(views.html.auth.bits.tor()).fuccess
else res
protected def NoEngine[A <: Result](a: => Fu[A])(implicit ctx: Context): Fu[Result] =
@ -347,8 +337,7 @@ private[controllers] trait LilaController
protected def authenticationFailed(implicit ctx: Context): Fu[Result] =
negotiate(
html = fuccess {
implicit val req = ctx.req
Redirect(routes.Auth.signup) withCookies LilaCookie.session(Env.security.api.AccessUri, req.uri)
Redirect(routes.Auth.signup) withCookies LilaCookie.session(Env.security.api.AccessUri, ctx.req.uri)
},
api = _ => ensureSessionId(ctx.req) {
Unauthorized(jsonError("Login required"))
@ -361,7 +350,7 @@ private[controllers] trait LilaController
html =
if (HTTPRequest isSynchronousHttp ctx.req) fuccess {
lila.mon.http.response.code403()
Forbidden(views.html.base.authFailed())
Forbidden(views.html.site.message.authFailed)
}
else fuccess(Results.Forbidden("Authorization failed")),
api = _ => fuccess(forbiddenJsonResult)
@ -371,8 +360,8 @@ private[controllers] trait LilaController
if (req.session.data.contains(LilaCookie.sessionId)) res
else res withCookies LilaCookie.makeSessionId(req)
protected def negotiate(html: => Fu[Result], api: ApiVersion => Fu[Result])(implicit ctx: Context): Fu[Result] =
lila.api.Mobile.Api.requestVersion(ctx.req).fold(html) { v =>
protected def negotiate(html: => Fu[Result], api: ApiVersion => Fu[Result])(implicit req: RequestHeader): Fu[Result] =
lila.api.Mobile.Api.requestVersion(req).fold(html) { v =>
api(v) dmap (_ as JSON)
}.dmap(_.withHeaders("Vary" -> "Accept"))
@ -430,7 +419,9 @@ private[controllers] trait LilaController
}
} dmap {
case Some(d) if !lila.common.PlayApp.isProd =>
Some(d.copy(user = d.user.addRole(lila.security.Permission.Beta.name)))
d.copy(user = d.user
.addRole(lila.security.Permission.Beta.name)
.addRole(lila.security.Permission.Prismic.name)).some
case d => d
} flatMap {
case None => fuccess(None -> None)

View File

@ -69,7 +69,7 @@ object Main extends LilaController {
def mobile = Open { implicit ctx =>
pageHit
OptionOk(Prismic getBookmark "mobile-apk") {
case (doc, resolver) => html.mobile.home(doc, resolver)
case (doc, resolver) => html.mobile(doc, resolver)
}
}
@ -135,10 +135,6 @@ Disallow: /games/export
}
}
val freeJs = Open { implicit ctx =>
Ok(html.site.freeJs(ctx)).fuccess
}
def renderNotFound(req: RequestHeader): Fu[Result] =
reqToCtx(req) map renderNotFound
@ -147,12 +143,8 @@ Disallow: /games/export
NotFound(html.base.notFound()(ctx))
}
def fpmenu = Open { implicit ctx =>
Ok(html.base.fpmenu()).fuccess
}
def getFishnet = Open { implicit ctx =>
Ok(html.site.getFishnet()).fuccess
Ok(html.site.bits.getFishnet()).fuccess
}
def costs = Open { implicit ctx =>
@ -181,6 +173,7 @@ Disallow: /games/export
case 87 => routes.Stat.ratingDistribution("blitz").url
case 110 => s"$faq#name"
case 29 => s"$faq#titles"
case 4811 => s"$faq#lm"
case 216 => routes.Main.mobile.url
case 340 => s"$faq#trophies"
case 6 => s"$faq#ratings"

View File

@ -3,8 +3,8 @@ package controllers
import play.api.data.Form
import play.api.libs.json._
import play.api.mvc.Result
import play.twirl.api.Html
import scala.concurrent.duration._
import scalatags.Text.Frag
import lila.api.Context
import lila.app._
@ -47,7 +47,7 @@ object Message extends LilaController {
negotiate(
html = OptionFuOk(api.thread(id, me)) { thread =>
relationApi.fetchBlocks(thread otherUserId me, me.id) map { blocked =>
val form = !thread.isTooBig option forms.post
val form = thread.isReplyable option forms.post
html.message.thread(thread, form, blocked)
}
} map NoCache,
@ -131,7 +131,7 @@ object Message extends LilaController {
}
}
private def renderForm(me: UserModel, title: Option[String], f: Form[_] => Form[_])(implicit ctx: Context): Fu[Html] =
private def renderForm(me: UserModel, title: Option[String], f: Form[_] => Form[_])(implicit ctx: Context): Fu[Frag] =
get("user") ?? UserRepo.named flatMap { user =>
user.fold(fuTrue)(u => security.canMessage(me.id, u.id)) map { canMessage =>
html.message.form(

View File

@ -7,6 +7,7 @@ import lila.common.{ IpAddress, EmailAddress, HTTPRequest }
import lila.report.{ Suspect, Mod => AsMod, SuspectId }
import lila.security.Permission
import lila.user.{ UserRepo, User => UserModel, Title }
import lila.mod.UserSearch
import ornicar.scalalib.Zero
import views._
@ -236,13 +237,18 @@ object Mod extends LilaController {
def search = SecureBody(_.UserSearch) { implicit ctx => me =>
implicit def req = ctx.body
val f = lila.mod.UserSearch.form
val f = UserSearch.form
f.bindFromRequest.fold(
err => BadRequest(html.mod.search(err, Nil)).fuccess,
query => Env.mod.search(query) map { html.mod.search(f.fill(query), _) }
)
}
protected[controllers] def searchTerm(q: String)(implicit ctx: Context) = {
val query = UserSearch exact q
Env.mod.search(query) map { users => Ok(html.mod.search(UserSearch.form fill query, users)) }
}
def chatUser(username: String) = Secure(_.ChatTimeout) { implicit ctx => me =>
implicit val lightUser = Env.user.lightUserSync _
JsonOptionOk {
@ -267,7 +273,7 @@ object Mod extends LilaController {
)).bindFromRequest.fold(
err => BadRequest(html.mod.permissions(user)).fuccess,
permissions =>
modApi.setPermissions(me.id, user.username, Permission(permissions)) >> {
modApi.setPermissions(AsMod(me), user.username, Permission(permissions)) >> {
(Permission(permissions) diff Permission(user.roles) contains Permission.Coach) ??
Env.security.automaticEmail.onBecomeCoach(user)
} >> {
@ -285,7 +291,7 @@ object Mod extends LilaController {
val email = query.headOption.map(EmailAddress.apply) flatMap Env.security.emailAddressValidator.validate
val username = query lift 1
def tryWith(setEmail: EmailAddress, q: String): Fu[Option[Result]] =
Env.mod.search(lila.mod.UserSearch.exact(q)) flatMap {
Env.mod.search(UserSearch.exact(q)) flatMap {
case List(UserModel.WithEmails(user, _)) => (!user.everLoggedIn).?? {
lila.mon.user.register.modConfirmEmail()
modApi.setEmail(me.id, user.id, setEmail)

View File

@ -19,13 +19,13 @@ object OAuthApp extends LilaController {
}
def create = Auth { implicit ctx => me =>
Ok(html.oAuth.app.create(env.forms.app.create)).fuccess
Ok(html.oAuth.app.form.create(env.forms.app.create)).fuccess
}
def createApply = AuthBody { implicit ctx => me =>
implicit val req = ctx.body
env.forms.app.create.bindFromRequest.fold(
err => BadRequest(html.oAuth.app.create(err)).fuccess,
err => BadRequest(html.oAuth.app.form.create(err)).fuccess,
setup => {
val app = setup make me
env.appApi.create(app) inject Redirect(routes.OAuthApp.edit(app.clientId.value))
@ -35,7 +35,7 @@ object OAuthApp extends LilaController {
def edit(id: String) = Auth { implicit ctx => me =>
OptionFuResult(env.appApi.findBy(App.Id(id), me)) { app =>
Ok(html.oAuth.app.edit(app, env.forms.app.edit(app))).fuccess
Ok(html.oAuth.app.form.edit(app, env.forms.app.edit(app))).fuccess
}
}
@ -43,7 +43,7 @@ object OAuthApp extends LilaController {
OptionFuResult(env.appApi.findBy(App.Id(id), me)) { app =>
implicit val req = ctx.body
env.forms.app.edit(app).bindFromRequest.fold(
err => BadRequest(html.oAuth.app.edit(app, err)).fuccess,
err => BadRequest(html.oAuth.app.form.edit(app, err)).fuccess,
data => env.appApi.update(app) { data.update(_) } map { r => Redirect(routes.OAuthApp.edit(app.clientId.value)) }
)
}

View File

@ -28,6 +28,13 @@ object Page extends LilaController {
}
}
def source = Open { implicit ctx =>
pageHit
OptionOk(Prismic getBookmark "source") {
case (doc, resolver) => views.html.site.help.source(doc, resolver)
}
}
def swag = Open { implicit ctx =>
pageHit
OptionOk(Prismic getBookmark "swag") {

View File

@ -31,8 +31,7 @@ object Pref extends LilaController {
}
def formApply = AuthBody { implicit ctx => me =>
def onSuccess(data: lila.pref.DataForm.PrefData) =
api.setPref(data(ctx.pref), notifyChange = true) inject Ok("saved")
def onSuccess(data: lila.pref.DataForm.PrefData) = api.setPref(data(ctx.pref)) inject Ok("saved")
implicit val req = ctx.body
forms.pref.bindFromRequest.fold(
err => forms.pref.bindFromRequest(lila.pref.FormCompatLayer(ctx.body)).fold(
@ -43,16 +42,16 @@ object Pref extends LilaController {
)
}
def setZoom = Action { implicit req =>
val zoom = getInt("v", req) | 100
Ok(()).withCookies(LilaCookie.session("zoom", zoom.toString))
}
def set(name: String) = OpenBody { implicit ctx =>
implicit val req = ctx.body
(setters get name) ?? {
case (form, fn) => FormResult(form) { v =>
fn(v, ctx) map { cookie => Ok(()).withCookies(cookie) }
if (name == "zoom") {
Ok.withCookies(LilaCookie.session("zoom", (getInt("v") | 180).toString)).fuccess
} else {
implicit val req = ctx.body
(setters get name) ?? {
case (form, fn) => FormResult(form) { v =>
fn(v, ctx) map { cookie => Ok(()).withCookies(cookie) }
}
}
}
}
@ -75,6 +74,6 @@ object Pref extends LilaController {
private def save(name: String)(value: String, ctx: Context): Fu[Cookie] =
ctx.me ?? {
api.setPrefString(_, name, value, notifyChange = false)
api.setPrefString(_, name, value)
} inject LilaCookie.session(name, value)(ctx.req)
}

View File

@ -5,6 +5,7 @@ import play.api.mvc._
import lila.api.Context
import lila.app._
import lila.common.{ HTTPRequest, IpAddress, MaxPerSecond }
import lila.game.PgnDump
import lila.puzzle.{ PuzzleId, Result, Puzzle => PuzzleModel, UserInfos }
import lila.user.UserRepo
@ -215,18 +216,37 @@ object Puzzle extends LilaController {
)
}
/* For BC */
def embed = Action { req =>
Ok {
val bg = get("bg", req) | "light"
val theme = get("theme", req) | "brown"
val url = s"""${req.domain + routes.Puzzle.frame}?bg=$bg&theme=$theme"""
s"""document.write("<iframe src='https://$url&embed=" + document.domain + "' class='lichess-training-iframe' allowtransparency='true' frameBorder='0' style='width: 224px; height: 264px;' title='Lichess free online chess'></iframe>");"""
s"""document.write("<iframe src='https://$url&embed=" + document.domain + "' class='lichess-training-iframe' allowtransparency='true' frameborder='0' style='width: 224px; height: 264px;' title='Lichess free online chess'></iframe>");"""
} as JAVASCRIPT withHeaders (CACHE_CONTROL -> "max-age=86400")
}
def frame = Open { implicit ctx =>
OptionOk(env.daily.get) { daily =>
html.puzzle.embed(daily)
def frame = Action.async { implicit req =>
env.daily.get map {
case None => NotFound
case Some(daily) => html.puzzle.embed(daily)
}
}
def activity = Scoped(_.Puzzle.Read) { req => me =>
Api.GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) {
Api.GlobalLinearLimitPerUserOption(me.some) {
val config = lila.puzzle.PuzzleActivity.Config(
user = me,
max = getInt("max", req) map (_ atLeast 1),
perSecond = MaxPerSecond(20)
)
Ok.chunked(env.activity.stream(config)).withHeaders(
noProxyBufferHeader,
CONTENT_TYPE -> ndJsonContentType
).fuccess
}
}
}
}

View File

@ -35,7 +35,13 @@ object Relation extends LilaController {
}
def follow(userId: String) = Auth { implicit ctx => me =>
env.api.follow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
env.api.reachedMaxFollowing(me.id) flatMap {
case true => Env.message.api.sendPresetFromLichess(
me,
lila.message.ModPreset.maxFollow(me.username, Env.relation.MaxFollow)
).void
case _ => env.api.follow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
}
}
def unfollow(userId: String) = Auth { implicit ctx => me =>
@ -56,7 +62,7 @@ object Relation extends LilaController {
RelatedPager(env.api.followingPaginatorAdapter(user.id), page) flatMap { pag =>
negotiate(
html = env.api countFollowers user.id map { nbFollowers =>
Ok(html.relation.following(user, pag, nbFollowers))
Ok(html.relation.bits.following(user, pag, nbFollowers))
},
api = _ => Ok(jsonRelatedPaginator(pag)).fuccess
)
@ -71,7 +77,7 @@ object Relation extends LilaController {
RelatedPager(env.api.followersPaginatorAdapter(user.id), page) flatMap { pag =>
negotiate(
html = env.api countFollowing user.id map { nbFollowing =>
Ok(html.relation.followers(user, pag, nbFollowing))
Ok(html.relation.bits.followers(user, pag, nbFollowing))
},
api = _ => Ok(jsonRelatedPaginator(pag)).fuccess
)
@ -112,7 +118,7 @@ object Relation extends LilaController {
def blocks(page: Int) = Auth { implicit ctx => me =>
Reasonable(page, 20) {
RelatedPager(env.api.blockingPaginatorAdapter(me.id), page) map { pag =>
html.relation.blocks(me, pag)
html.relation.bits.blocks(me, pag)
}
}
}

View File

@ -23,14 +23,14 @@ object Relay extends LilaController {
def form = Auth { implicit ctx => me =>
NoLame {
Ok(html.relay.create(env.forms.create)).fuccess
Ok(html.relay.form.create(env.forms.create)).fuccess
}
}
def create = AuthBody { implicit ctx => me =>
implicit val req = ctx.body
env.forms.create.bindFromRequest.fold(
err => BadRequest(html.relay.create(err)).fuccess,
err => BadRequest(html.relay.form.create(err)).fuccess,
setup => env.api.create(setup, me) map { relay =>
Redirect(showRoute(relay))
}
@ -39,7 +39,7 @@ object Relay extends LilaController {
def edit(slug: String, id: String) = Auth { implicit ctx => me =>
OptionFuResult(env.api.byIdAndContributor(id, me)) { relay =>
Ok(html.relay.edit(relay, env.forms.edit(relay))).fuccess
Ok(html.relay.form.edit(relay, env.forms.edit(relay))).fuccess
}
}
@ -47,7 +47,7 @@ object Relay extends LilaController {
OptionFuResult(env.api.byIdAndContributor(id, me)) { relay =>
implicit val req = ctx.body
env.forms.edit(relay).bindFromRequest.fold(
err => BadRequest(html.relay.edit(relay, err)).fuccess,
err => BadRequest(html.relay.form.edit(relay, err)).fuccess,
data => env.api.update(relay) { data.update(_, me) } map { r =>
Redirect(showRoute(r))
}

View File

@ -73,7 +73,7 @@ object Round extends LilaController with TheftPrevention {
simul foreach Env.simul.api.onPlayerConnection(pov.game, ctx.me)
Ok(html.round.player(pov, data,
tour = tour,
simul = simul.filter(_ isHost ctx.me),
simul = simul,
cross = crosstable,
playing = playing,
chatOption = chatOption,
@ -192,7 +192,7 @@ object Round extends LilaController with TheftPrevention {
else for { // web crawlers don't need the full thing
initialFen <- GameRepo.initialFen(pov.gameId)
pgn <- Env.api.pgnDump(pov.game, initialFen, none, PgnDump.WithFlags(clocks = false))
} yield Ok(html.round.watcherBot(pov, initialFen, pgn))
} yield Ok(html.round.watcher.crawler(pov, initialFen, pgn))
}.mon(_.http.response.watcher.website),
api = apiVersion => for {
data <- Env.api.roundApi.watcher(pov, apiVersion, tv = none)
@ -210,7 +210,7 @@ object Round extends LilaController with TheftPrevention {
tourId ?? { Env.tournament.api.miniView(_, withTop) }
private[controllers] def getWatcherChat(game: GameModel)(implicit ctx: Context): Fu[Option[lila.chat.UserChat.Mine]] = {
ctx.noKid && ctx.me.exists(Env.chat.panic.allowed) && {
ctx.noKid && ctx.me.fold(true)(Env.chat.panic.allowed) && {
game.finishedOrAborted || !ctx.userId.exists(game.userIds.contains)
}
} ?? {
@ -222,7 +222,7 @@ object Round extends LilaController with TheftPrevention {
private[controllers] def getPlayerChat(game: GameModel, tour: Option[Tour])(implicit ctx: Context): Fu[Option[Chat.GameOrEvent]] = ctx.noKid ?? {
(game.tournamentId, game.simulId) match {
case (Some(tid), _) => {
ctx.isAuth && tour.fold(true)(Tournament.canHaveChat)
ctx.isAuth && tour.fold(true)(Tournament.canHaveChat(_, none))
} ??
Env.chat.api.userChat.cached.findMine(Chat.Id(tid), ctx.me).map { chat =>
Chat.GameOrEvent(Right(chat truncate 50)).some
@ -294,14 +294,10 @@ object Round extends LilaController with TheftPrevention {
}
def mini(gameId: String, color: String) = Open { implicit ctx =>
OptionOk(GameRepo.pov(gameId, color)) { pov =>
html.game.bits.mini(pov)
}
OptionOk(GameRepo.pov(gameId, color))(html.game.bits.mini)
}
def miniFullId(fullId: String) = Open { implicit ctx =>
OptionOk(GameRepo pov fullId) { pov =>
html.game.bits.mini(pov)
}
OptionOk(GameRepo pov fullId)(html.game.bits.mini)
}
}

View File

@ -218,7 +218,6 @@ object Setup extends LilaController with TheftPrevention {
}
private[controllers] def redirectPov(pov: Pov)(implicit ctx: Context) = {
implicit val req = ctx.req
val redir = Redirect(routes.Round.watcher(pov.gameId, "white"))
if (ctx.isAuth) redir
else redir withCookies LilaCookie.cookie(

View File

@ -5,9 +5,9 @@ import play.api.mvc._
import lila.api.Context
import lila.app._
import lila.chat.Chat
import lila.common.HTTPRequest
import lila.simul.{ Simul => Sim }
import lila.chat.Chat
import views._
object Simul extends LilaController {
@ -48,36 +48,50 @@ object Simul extends LilaController {
} map NoCache
}
private[controllers] def canHaveChat(implicit ctx: Context): Boolean = ctx.me ?? { u =>
if (ctx.kid) false
else Env.chat.panic allowed u
}
private[controllers] def canHaveChat(implicit ctx: Context): Boolean =
!ctx.kid && // no public chats for kids
ctx.me.fold(true) { // anon can see public chats
Env.chat.panic.allowed
}
def start(simulId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api start simul.id
Ok(Json.obj("ok" -> true)) as JSON
jsonOkResult
}
}
def abort(simulId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api abort simul.id
Ok(Json.obj("ok" -> true)) as JSON
jsonOkResult
}
}
def accept(simulId: String, userId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api.accept(simul.id, userId, true)
Ok(Json.obj("ok" -> true)) as JSON
jsonOkResult
}
}
def reject(simulId: String, userId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api.accept(simul.id, userId, false)
Ok(Json.obj("ok" -> true)) as JSON
jsonOkResult
}
}
def setText(simulId: String) = OpenBody { implicit ctx =>
AsHost(simulId) { simul =>
implicit val req = ctx.body
env.forms.setText.bindFromRequest.fold(
err => BadRequest,
text => {
env.api.setText(simul.id, text)
jsonOkResult
}
)
}
}

View File

@ -47,7 +47,7 @@ object Streamer extends LilaController {
def create = AuthBody { implicit ctx => me =>
NoLame {
NoShadowban {
api.find(me) flatMap {
api find me flatMap {
case None => api.create(me) inject Redirect(routes.Streamer.edit)
case _ => Redirect(routes.Streamer.edit).fuccess
}
@ -120,7 +120,7 @@ object Streamer extends LilaController {
private def AsStreamer(f: StreamerModel.WithUser => Fu[Result])(implicit ctx: Context) =
ctx.me.fold(notFound) { me =>
api.find(get("u").ifTrue(isGranted(_.Streamers)) | me.id) flatMap {
_.fold(Ok(html.streamer.create(me)).fuccess)(f)
_.fold(Ok(html.streamer.bits.create(me)).fuccess)(f)
}
}

View File

@ -32,7 +32,7 @@ object Study extends LilaController {
}
else Env.studySearch(ctx.me)(text, page) flatMap { pag =>
negotiate(
html = Ok(html.study.search(pag, text)).fuccess,
html = Ok(html.study.list.search(pag, text)).fuccess,
api = _ => apiStudies(pag)
)
}
@ -209,10 +209,12 @@ object Study extends LilaController {
}
}
private[controllers] def chatOf(study: lila.study.Study)(implicit ctx: Context) =
(ctx.noKid && ctx.me.exists { me =>
study.isMember(me.id) || Env.chat.panic.allowed(me)
}) ?? Env.chat.api.userChat.findMine(Chat.Id(study.id.value), ctx.me).map(some)
private[controllers] def chatOf(study: lila.study.Study)(implicit ctx: Context) = {
!ctx.kid && // no public chats for kids
ctx.me.fold(true) { // anon can see public chats
Env.chat.panic.allowed
}
} ?? Env.chat.api.userChat.findMine(Chat.Id(study.id.value), ctx.me).map(some)
def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx =>
get("sri") ?? { uid =>
@ -278,17 +280,17 @@ object Study extends LilaController {
)
}
def embed(id: String, chapterId: String) = Open { implicit ctx =>
env.api.byIdWithChapter(id, chapterId) flatMap {
def embed(id: String, chapterId: String) = Action.async { implicit req =>
env.api.byIdWithChapter(id, chapterId).map(_.filterNot(_.study.isPrivate)) flatMap {
_.fold(embedNotFound) {
case WithChapter(study, chapter) => CanViewResult(study) {
case WithChapter(study, chapter) =>
env.jsonView(study.copy(
members = lila.study.StudyMembers(Map.empty) // don't need no members
), List(chapter.metadata), chapter, ctx.me) flatMap { studyJson =>
), List(chapter.metadata), chapter, none) flatMap { studyJson =>
val setup = chapter.setup
val initialFen = chapter.root.fen.some
val pov = UserAnalysis.makePov(initialFen, setup.variant)
val baseData = Env.round.jsonView.userAnalysisJson(pov, ctx.pref, initialFen, setup.orientation, owner = false, me = ctx.me)
val baseData = Env.round.jsonView.userAnalysisJson(pov, lila.pref.Pref.default, initialFen, setup.orientation, owner = false, me = none)
val analysis = baseData ++ Json.obj(
"treeParts" -> partitionTreeJsonWriter.writes {
lila.study.TreeBuilder.makeRoot(chapter.root, setup.variant)
@ -303,13 +305,12 @@ object Study extends LilaController {
api = _ => Ok(Json.obj("study" -> data.study, "analysis" -> data.analysis)).fuccess
)
}
}
}
} map NoCache
}
private def embedNotFound(implicit ctx: Context): Fu[Result] =
fuccess(NotFound(html.study.embed.notFound()))
private def embedNotFound(implicit req: RequestHeader): Fu[Result] =
fuccess(NotFound(html.study.embed.notFound))
def cloneStudy(id: String) = Auth { implicit ctx => me =>
OptionFuResult(env.api.byId(id)) { study =>

View File

@ -19,7 +19,7 @@ object Team extends LilaController {
def all(page: Int) = Open { implicit ctx =>
NotForKids {
paginator popularTeams page map { html.team.all(_) }
paginator popularTeams page map { html.team.list.all(_) }
}
}
@ -40,8 +40,8 @@ object Team extends LilaController {
def search(text: String, page: Int) = OpenBody { implicit ctx =>
NotForKids {
if (text.trim.isEmpty) paginator popularTeams page map { html.team.all(_) }
else Env.teamSearch(text, page) map { html.team.search(text, _) }
if (text.trim.isEmpty) paginator popularTeams page map { html.team.list.all(_) }
else Env.teamSearch(text, page) map { html.team.list.search(text, _) }
}
}
@ -68,7 +68,7 @@ object Team extends LilaController {
def edit(id: String) = Auth { implicit ctx => me =>
OptionFuResult(api team id) { team =>
Owner(team) { fuccess(html.team.edit(team, forms edit team)) }
Owner(team) { fuccess(html.team.form.edit(team, forms edit team)) }
}
}
@ -77,7 +77,7 @@ object Team extends LilaController {
Owner(team) {
implicit val req = ctx.body
forms.edit(team).bindFromRequest.fold(
err => BadRequest(html.team.edit(team, err)).fuccess,
err => BadRequest(html.team.form.edit(team, err)).fuccess,
data => api.update(team, data, me) inject Redirect(routes.Team.show(team.id))
)
}
@ -88,7 +88,7 @@ object Team extends LilaController {
OptionFuResult(api team id) { team =>
Owner(team) {
MemberRepo userIdsByTeam team.id map { userIds =>
html.team.kick(team, userIds - me.id)
html.team.admin.kick(team, userIds - me.id)
}
}
}
@ -98,7 +98,7 @@ object Team extends LilaController {
OptionFuResult(api team id) { team =>
Owner(team) {
implicit val req = ctx.body
forms.selectMember.bindFromRequest.value ?? { api.kick(team, _, me) } inject Redirect(routes.Team.show(team.id))
forms.selectMember.bindFromRequest.value.pp ?? { api.kick(team, _, me) } inject Redirect(routes.Team.show(team.id))
}
}
}
@ -107,7 +107,7 @@ object Team extends LilaController {
OptionFuResult(api team id) { team =>
Owner(team) {
MemberRepo userIdsByTeam team.id map { userIds =>
html.team.changeOwner(team, userIds - team.createdBy)
html.team.admin.changeOwner(team, userIds - team.createdBy)
}
}
}
@ -134,7 +134,7 @@ object Team extends LilaController {
NotForKids {
OnePerWeek(me) {
forms.anyCaptcha map { captcha =>
Ok(html.team.form(forms.create, captcha))
Ok(html.team.form.create(forms.create, captcha))
}
}
}
@ -145,7 +145,7 @@ object Team extends LilaController {
implicit val req = ctx.body
forms.create.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
BadRequest(html.team.form(err, captcha))
BadRequest(html.team.form.create(err, captcha))
},
data => api.create(data, me) ?? {
_ map { team => Redirect(routes.Team.show(team.id)): Result }
@ -155,7 +155,7 @@ object Team extends LilaController {
}
def mine = Auth { implicit ctx => me =>
api mine me map { html.team.mine(_) }
api mine me map { html.team.list.mine(_) }
}
def join(id: String) = Auth { implicit ctx => implicit me =>
@ -168,12 +168,12 @@ object Team extends LilaController {
def requests = Auth { implicit ctx => me =>
Env.team.cached.nbRequests invalidate me.id
api requestsWithUsers me map { html.team.allRequests(_) }
api requestsWithUsers me map { html.team.request.all(_) }
}
def requestForm(id: String) = Auth { implicit ctx => me =>
OptionFuOk(api.requestable(id, me)) { team =>
forms.anyCaptcha map { html.team.requestForm(team, forms.request, _) }
forms.anyCaptcha map { html.team.request.requestForm(team, forms.request, _) }
}
}
@ -182,7 +182,7 @@ object Team extends LilaController {
implicit val req = ctx.body
forms.request.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
BadRequest(html.team.requestForm(team, err, captcha))
BadRequest(html.team.request.requestForm(team, err, captcha))
},
setup => api.createRequest(team, setup, me) inject Redirect(routes.Team.show(team.id))
)

View File

@ -19,7 +19,7 @@ object Tournament extends LilaController {
private def env = Env.tournament
private def repo = TournamentRepo
private def tournamentNotFound(implicit ctx: Context) = NotFound(html.tournament.notFound())
private def tournamentNotFound(implicit ctx: Context) = NotFound(html.tournament.bits.notFound())
private[controllers] val upcomingCache = Env.memo.asyncCache.single[(VisibleTournaments, List[Tour])](
name = "tournament.home",
@ -64,7 +64,7 @@ object Tournament extends LilaController {
case "arena" => System.Arena.some
case _ => none
}
Ok(html.tournament.faqPage(system)).fuccess
Ok(html.tournament.faq.page(system)).fuccess
}
def leaderboard = Open { implicit ctx =>
@ -74,11 +74,14 @@ object Tournament extends LilaController {
} yield Ok(html.tournament.leaderboard(winners))
}
private[controllers] def canHaveChat(tour: Tour)(implicit ctx: Context): Boolean = ctx.me ?? { u =>
if (ctx.kid) false
else if (tour.isPrivate) true
else Env.chat.panic.allowed(u, tighter = tour.variant == chess.variant.Antichess)
}
private[controllers] def canHaveChat(tour: Tour, json: Option[JsObject])(implicit ctx: Context): Boolean =
!ctx.kid && // no public chats for kids
ctx.me.fold(!tour.isPrivate) { u => // anon can see public chats, except for private tournaments
(!tour.isPrivate || json.fold(true)(jsonHasMe)) && // private tournament that I joined
Env.chat.panic.allowed(u, tighter = tour.variant == chess.variant.Antichess)
}
private def jsonHasMe(js: JsObject): Boolean = (js \ "me").toOption.isDefined
def show(id: String) = Open { implicit ctx =>
val page = getInt("page")
@ -88,8 +91,8 @@ object Tournament extends LilaController {
(for {
verdicts <- env.api.verdicts(tour, ctx.me, getUserTeamIds)
version <- env.version(tour.id)
chat <- canHaveChat(tour) ?? Env.chat.api.userChat.cached.findMine(Chat.Id(tour.id), ctx.me).map(some)
json <- env.jsonView(tour, page, ctx.me, getUserTeamIds, none, version.some, partial = false, ctx.lang)
chat <- canHaveChat(tour, json.some) ?? Env.chat.api.userChat.cached.findMine(Chat.Id(tour.id), ctx.me).map(some)
_ <- chat ?? { c => Env.user.lightUserApi.preloadMany(c.chat.userIds) }
streamers <- streamerCache get tour.id
shieldOwner <- env.shieldApi currentOwner tour
@ -284,11 +287,19 @@ object Tournament extends LilaController {
def shields = Open { implicit ctx =>
for {
history <- env.shieldApi.history
history <- env.shieldApi.history(5.some)
_ <- Env.user.lightUserApi preloadMany history.userIds
} yield html.tournament.shields(history)
}
def categShields(k: String) = Open { implicit ctx =>
OptionFuOk(env.shieldApi.byCategKey(k)) {
case (categ, awards) =>
Env.user.lightUserApi preloadMany awards.map(_.owner.value) inject
html.tournament.shields.byCateg(categ, awards)
}
}
def calendar = Open { implicit ctx =>
env.api.calendar map { tours =>
Ok(html.tournament.calendar(env.scheduleJsonView calendar tours))

View File

@ -15,16 +15,11 @@ object Tv extends LilaController {
(lila.tv.Tv.Channel.byKey get chanKey).fold(notFound)(lichessTv)
}
def sides(chanKey: String, gameId: String, color: String) = Open { implicit ctx =>
lila.tv.Tv.Channel.byKey get chanKey match {
case None => notFound
case Some(channel) =>
OptionFuResult(GameRepo.pov(gameId, color)) { pov =>
Env.tv.tv.getChampions zip
Env.game.crosstableApi.withMatchup(pov.game) map {
case (champions, crosstable) => Ok(html.tv.side.sides(channel, champions, pov, crosstable))
}
}
def sides(gameId: String, color: String) = Open { implicit ctx =>
OptionFuResult(GameRepo.pov(gameId, color)) { pov =>
Env.game.crosstableApi.withMatchup(pov.game) map { ct =>
Ok(html.tv.side.sides(pov, ct))
}
}
}
@ -62,7 +57,7 @@ object Tv extends LilaController {
def gamesChannel(chanKey: String) = Open { implicit ctx =>
(lila.tv.Tv.Channel.byKey get chanKey) ?? { channel =>
Env.tv.tv.getChampions zip Env.tv.tv.getGames(channel, 9) map {
Env.tv.tv.getChampions zip Env.tv.tv.getGames(channel, 12) map {
case (champs, games) => NoCache {
Ok(html.tv.games(channel, games map lila.game.Pov.first, champs))
}
@ -81,23 +76,19 @@ object Tv extends LilaController {
}
}
/* for BC */
def embed = Action { req =>
Ok {
val bg = get("bg", req) | "light"
val theme = get("theme", req) | "brown"
val url = s"""${req.domain + routes.Tv.frame}?bg=$bg&theme=$theme"""
s"""document.write("<iframe src='https://$url&embed=" + document.domain + "' class='lichess-tv-iframe' allowtransparency='true' frameBorder='0' style='width: 224px; height: 264px;' title='Lichess free online chess'></iframe>");"""
val config = ui.EmbedConfig(req)
val url = s"""${req.domain + routes.Tv.frame}?bg=${config.bg}&theme=${config.board}"""
s"""document.write("<iframe src='https://$url&embed=" + document.domain + "' class='lichess-tv-iframe' allowtransparency='true' frameborder='0' style='width: 224px; height: 264px;' title='Lichess free online chess'></iframe>");"""
} as JAVASCRIPT withHeaders (CACHE_CONTROL -> "max-age=86400")
}
def frame = Action.async { implicit req =>
Env.tv.tv.getBestGame map {
case None => NotFound
case Some(game) => Ok(views.html.tv.embed(
Pov first game,
get("bg", req) | "light",
lila.pref.Theme(~get("theme", req)).cssClass
))
case Some(game) => Ok(views.html.tv.embed(Pov first game))
}
}
}

View File

@ -4,7 +4,6 @@ import play.api.data.Form
import play.api.libs.iteratee._
import play.api.libs.json._
import play.api.mvc._
import play.twirl.api.Html
import scala.concurrent.duration._
import lila.api.{ Context, BodyContext }
@ -68,7 +67,7 @@ object User extends LilaController {
nbs Env.current.userNbGames(u, ctx)
info Env.current.userInfo(u, nbs, ctx)
social Env.current.socialInfo(u, ctx)
} yield status(html.user.show.activity(u, as, info, social))
} yield status(html.user.show.page.activity(u, as, info, social))
}.mon(_.http.response.user.show.website)
else Env.activity.read.recent(u) map { as =>
status(html.activity(u, as))
@ -98,7 +97,7 @@ object User extends LilaController {
_ <- Env.team.cached.nameCache preloadMany info.teamIds
social Env.current.socialInfo(u, ctx)
searchForm = (filters.current == GameFilter.Search) option GameFilterMenu.searchForm(userGameSearch, filters.current)(ctx.body)
} yield html.user.show.games(u, info, pag, filters, searchForm, social)
} yield html.user.show.page.games(u, info, pag, filters, searchForm, social)
else fuccess(html.user.show.gamesContent(u, nbs, pag, filters, filter))
} yield res,
api = _ => apiGames(u, filter, page)
@ -108,12 +107,14 @@ object User extends LilaController {
}
private def EnabledUser(username: String)(f: UserModel => Fu[Result])(implicit ctx: Context): Fu[Result] =
OptionFuResult(UserRepo named username) { u =>
if (u.enabled || isGranted(_.UserSpy)) f(u)
else negotiate(
UserRepo named username flatMap {
case None if isGranted(_.UserSpy) => Mod.searchTerm(username.trim)
case None => notFound
case Some(u) if (u.enabled || isGranted(_.UserSpy)) => f(u)
case Some(u) => negotiate(
html = UserRepo isErased u flatMap { erased =>
if (erased.value) notFound
else NotFound(html.user.disabled(u)).fuccess
else NotFound(html.user.show.page.disabled(u)).fuccess
},
api = _ => fuccess(NotFound(jsonError("No such user, or account closed")))
)
@ -145,12 +146,12 @@ object User extends LilaController {
}
}
def online = Open { implicit req =>
def online = Action.async { implicit req =>
val max = 50
negotiate(
html = notFound,
html = notFoundJson(),
api = _ => env.cached.getTop50Online map { list =>
Ok(Json.toJson(list.take(getInt("nb").fold(10)(_ min max)).map(env.jsonView(_))))
Ok(Json.toJson(list.take(getInt("nb", req).fold(10)(_ min max)).map(env.jsonView(_))))
}
)
}
@ -291,7 +292,7 @@ object User extends LilaController {
}
val irwin = Env.irwin.api.reports.withPovs(user) map {
_ ?? { reps =>
html.irwin.irwinReport(reps).some
html.irwin.report(reps).some
}
}
val assess = Env.mod.assessApi.getPlayerAggregateAssessmentWithGames(user.id) flatMap {
@ -300,7 +301,7 @@ object User extends LilaController {
}
}
import play.api.libs.EventSource
implicit val extractor = EventSource.EventDataExtractor[Html](_.toString)
implicit val extractor = EventSource.EventDataExtractor[scalatags.Text.Frag](_.render)
Ok.chunked {
(Enumerator(html.user.mod.menu(user)) interleave
futureToEnumerator(parts.logTimeIfGt(s"$username parts", 2 seconds)) interleave

View File

@ -12,11 +12,11 @@ object UserTournament extends LilaController {
path match {
case "recent" =>
Env.tournament.leaderboardApi.recentByUser(user, page).map { entries =>
Ok(html.userTournament.recent(user, entries))
Ok(html.userTournament.bits.recent(user, entries))
}
case "best" =>
Env.tournament.leaderboardApi.bestByUser(user, page).map { entries =>
Ok(html.userTournament.best(user, entries))
Ok(html.userTournament.bits.best(user, entries))
}
case "chart" => Env.tournament.leaderboardApi.chart(user).map { data =>
Ok(html.userTournament.chart(user, data))

View File

@ -41,7 +41,7 @@ object Video extends LilaController {
def show(id: String) = Open { implicit ctx =>
WithUserControl { control =>
env.api.video.find(id) flatMap {
case None => fuccess(NotFound(html.video.notFound(control)))
case None => fuccess(NotFound(html.video.bits.notFound(control)))
case Some(video) => env.api.video.similar(ctx.me, video, 9) zip
ctx.userId.?? { userId =>
env.api.view.add(View.make(videoId = video.id, userId = userId))
@ -56,7 +56,7 @@ object Video extends LilaController {
def author(author: String) = Open { implicit ctx =>
WithUserControl { control =>
env.api.video.byAuthor(ctx.me, author, getInt("page") | 1) map { videos =>
Ok(html.video.author(author, videos, control))
Ok(html.video.bits.author(author, videos, control))
}
}
}
@ -64,7 +64,7 @@ object Video extends LilaController {
def tags = Open { implicit ctx =>
WithUserControl { control =>
env.api.tag.allPopular map { tags =>
Ok(html.video.tags(tags, control))
Ok(html.video.bits.tags(tags, control))
}
}
}

View File

@ -28,7 +28,7 @@ final class Preload(
lightUserApi: LightUserApi
) {
private type Response = (JsObject, Vector[Entry], List[MiniForumPost], List[Tournament], List[Event], List[Simul], Option[Game], List[User.LightPerf], List[Winner], Option[lila.puzzle.DailyPuzzle], LiveStreams.WithTitles, List[lila.blog.MiniPost], Option[TempBan], Option[Preload.CurrentGame], Int)
private type Response = (JsObject, Vector[Entry], List[MiniForumPost], List[Tournament], List[Event], List[Simul], Option[Game], List[User.LightPerf], List[Winner], Option[lila.puzzle.DailyPuzzle], LiveStreams.WithTitles, List[lila.blog.MiniPost], Option[TempBan], Option[Preload.CurrentGame], Int, List[Pov])
def apply(
posts: Fu[List[MiniForumPost]],
@ -46,16 +46,17 @@ final class Preload(
leaderboard(()) zip
tourneyWinners zip
(ctx.noBot ?? dailyPuzzle()) zip
liveStreams().dmap(_.autoFeatured.withTitles(lightUserApi)) zip
(ctx.userId ?? getPlayban) flatMap {
case (data, povs) ~ posts ~ tours ~ events ~ simuls ~ feat ~ entries ~ lead ~ tWinners ~ puzzle ~ streams ~ playban =>
liveStreams().dmap(_.autoFeatured withTitles lightUserApi) zip
(ctx.userId ?? getPlayban) zip
(ctx.blind ?? ctx.me ?? GameRepo.urgentGames) flatMap {
case (data, povs) ~ posts ~ tours ~ events ~ simuls ~ feat ~ entries ~ lead ~ tWinners ~ puzzle ~ streams ~ playban ~ blindGames =>
val currentGame = ctx.me ?? Preload.currentGameMyTurn(povs, lightUserApi.sync) _
lightUserApi.preloadMany {
tWinners.map(_.userId) :::
posts.flatMap(_.userId) :::
entries.flatMap(_.userIds).toList
} inject
(data, entries, posts, tours, events, simuls, feat, lead, tWinners, puzzle, streams, Env.blog.lastPostCache.apply, playban, currentGame, countRounds())
(data, entries, posts, tours, events, simuls, feat, lead, tWinners, puzzle, streams, Env.blog.lastPostCache.apply, playban, currentGame, countRounds(), blindGames)
}
}

View File

@ -1,3 +1,14 @@
package lila
package object app extends PackageObject with socket.WithSocket
import play.api.http._
import play.api.mvc.Codec
import scalatags.Text.Frag
package object app extends PackageObject with socket.WithSocket {
implicit def contentTypeOfFrag(implicit codec: Codec): ContentTypeOf[Frag] =
ContentTypeOf[Frag](Some(ContentTypes.HTML))
implicit def writeableOfFrag(implicit codec: Codec): Writeable[Frag] =
Writeable(frag => codec.encode(frag.render))
}

View File

@ -1,8 +1,7 @@
package lila.app
package templating
import play.twirl.api.Html
import lila.app.ui.ScalatagsTemplate._
import lila.user.UserContext
trait AiHelper { self: I18nHelper =>
@ -15,8 +14,8 @@ trait AiHelper { self: I18nHelper =>
s"$name$rating"
}
def aiNameHtml(level: Int, withRating: Boolean = true)(implicit ctx: UserContext) =
Html(aiName(level, withRating).replace(" ", "&nbsp;"))
def aiNameFrag(level: Int, withRating: Boolean = true)(implicit ctx: UserContext) =
raw(aiName(level, withRating).replace(" ", "&nbsp;"))
def aiRating(level: Int): Option[Int] = Env.fishnet.aiPerfApi.intRatings get level
}

View File

@ -3,18 +3,21 @@ package templating
import controllers.routes
import play.api.mvc.RequestHeader
import play.twirl.api.Html
import lila.api.Context
import lila.common.{ AssetVersion, ContentSecurityPolicy }
import lila.app.ui.ScalatagsTemplate._
import lila.common.{ Nonce, AssetVersion, ContentSecurityPolicy }
trait AssetHelper { self: I18nHelper with SecurityHelper =>
def isProd: Boolean
val siteDomain = lila.api.Env.current.Net.Domain
val assetDomain = lila.api.Env.current.Net.AssetDomain
val socketDomain = lila.api.Env.current.Net.SocketDomain
val sameAssetDomain = siteDomain == assetDomain
val assetBaseUrl = s"//$assetDomain"
def assetVersion = AssetVersion.current
@ -26,75 +29,72 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
def dbImageUrl(path: String) = s"$assetBaseUrl/image/$path"
def cssTag(name: String): Html = cssAt("stylesheets/" + name)
def cssTag(name: String)(implicit ctx: Context): Frag =
cssTagWithTheme(name, ctx.currentBg)
def cssTags(names: String*): Html = Html {
names.map { name =>
cssTag(name).body
} mkString ""
}
def cssTags(names: List[(String, Boolean)]): Html =
cssTags(names.collect { case (k, true) => k }: _*)
def cssTagWithTheme(name: String, theme: String): Frag =
cssAt(s"css/$name.$theme.${if (isProd) "min" else "dev"}.css")
def cssVendorTag(name: String) = cssAt("vendor/" + name)
def cssTagNoTheme(name: String)(implicit ctx: Context): Frag =
cssAt(s"css/$name.${if (isProd) "min" else "dev"}.css")
def cssAt(path: String): Html = Html {
s"""<link href="${assetUrl(path)}" type="text/css" rel="stylesheet"/>"""
}
private def cssAt(path: String): Frag =
link(href := assetUrl(path), tpe := "text/css", rel := "stylesheet")
def jsTag(name: String, async: Boolean = false) =
jsAt("javascripts/" + name, async = async)
def jsTag(name: String, defer: Boolean = false): Frag =
jsAt("javascripts/" + name, defer = defer)
def jsAt(path: String, async: Boolean = false): Html = Html {
val src = assetUrl(path)
s"""<script${if (async) " async defer" else ""} src="$src"></script>"""
}
/* about async & defer, see https://flaviocopes.com/javascript-async-defer/
* we want defer only, to ensure scripts are executed in order of declaration,
* so that round.js doesn't run before site.js */
def jsAt(path: String, defer: Boolean = false): Frag = script(
defer option deferAttr,
src := assetUrl(path)
)
val jQueryTag = Html {
val jQueryTag = raw {
s"""<script src="${staticUrl("javascripts/vendor/jquery.min.js")}"></script>"""
}
def roundTag = jsAt(s"compiled/lichess.round${isProd ?? (".min")}.js", async = true)
def roundTag = jsAt(s"compiled/lichess.round${isProd ?? (".min")}.js", defer = true)
def roundNvuiTag(implicit ctx: Context) = ctx.blind option
jsAt(s"compiled/lichess.round.nvui.min.js", async = true)
jsAt(s"compiled/lichess.round.nvui.min.js", defer = true)
def analyseTag = jsAt(s"compiled/lichess.analyse${isProd ?? (".min")}.js")
def analyseNvuiTag(implicit ctx: Context) = ctx.blind option
jsAt(s"compiled/lichess.analyse.nvui.min.js")
val highchartsLatestTag = Html {
def captchaTag = jsAt(s"compiled/captcha.js")
val highchartsLatestTag = raw {
s"""<script src="${staticUrl("vendor/highcharts-4.2.5/highcharts.js")}"></script>"""
}
val highchartsMoreTag = Html {
val highchartsMoreTag = raw {
s"""<script src="${staticUrl("vendor/highcharts-4.2.5/highcharts-more.js")}"></script>"""
}
val typeaheadTag = Html {
s"""<script src="${staticUrl("javascripts/vendor/typeahead.bundle.min.js")}"></script>"""
val fingerprintTag = raw {
s"""<script async src="${staticUrl("javascripts/vendor/fp2.min.js")}"></script>"""
}
val fingerprintTag = Html {
s"""<script async defer src="${staticUrl("javascripts/vendor/fp2.min.js")}"></script>"""
}
val flatpickrTag = Html {
s"""<script async defer src="${staticUrl("javascripts/vendor/flatpickr.min.js")}"></script>"""
}
val nonAsyncFlatpickrTag = Html {
val flatpickrTag = raw {
s"""<script defer src="${staticUrl("javascripts/vendor/flatpickr.min.js")}"></script>"""
}
def delayFlatpickrStart(implicit ctx: Context) = embedJs {
val nonAsyncFlatpickrTag = raw {
s"""<script defer src="${staticUrl("javascripts/vendor/flatpickr.min.js")}"></script>"""
}
def delayFlatpickrStart(implicit ctx: Context) = embedJsUnsafe {
"""$(function() { setTimeout(function() { $(".flatpickr").flatpickr(); }, 2000) });"""
}
val infiniteScrollTag = jsTag("vendor/jquery.infinitescroll.min.js")
def prismicJs(implicit ctx: Context) = Html {
def prismicJs(implicit ctx: Context): Frag = raw {
isGranted(_.Prismic) ?? {
embedJsUnsafe("""window.prismic={endpoint:'https://lichess.prismic.io/api/v2'}""").body ++
embedJsUnsafe("""window.prismic={endpoint:'https://lichess.prismic.io/api/v2'}""").render ++
"""<script type="text/javascript" src="//static.cdn.prismic.io/prismic.min.js"></script>"""
}
}
@ -105,7 +105,7 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
ContentSecurityPolicy(
defaultSrc = List("'self'", assets),
connectSrc = List("'self'", assets, socket, lila.api.Env.current.ExplorerEndpoint, lila.api.Env.current.TablebaseEndpoint),
styleSrc = List("'self'", "'unsafe-inline'", assets, "https://fonts.googleapis.com"),
styleSrc = List("'self'", "'unsafe-inline'", assets),
fontSrc = List("'self'", assetDomain, "https://fonts.gstatic.com"),
frameSrc = List("'self'", assets, "https://www.youtube.com"),
workerSrc = List("'self'", assets),
@ -120,15 +120,12 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
ctx.nonce.fold(csp)(csp.withNonce(_))
}
def embedJsUnsafe(js: String)(implicit ctx: Context): Html = Html {
val nonce = ctx.nonce ?? { nonce => s""" nonce="$nonce"""" }
s"""<script$nonce>$js</script>"""
}
def embedJsUnsafe(js: scalatags.Text.RawFrag)(implicit ctx: Context): scalatags.Text.RawFrag = scalatags.Text.all.raw {
def embedJsUnsafe(js: String)(implicit ctx: Context): Frag = raw {
val nonce = ctx.nonce ?? { nonce => s""" nonce="$nonce"""" }
s"""<script$nonce>$js</script>"""
}
def embedJs(js: Html)(implicit ctx: Context): Html = embedJsUnsafe(js.body)
def embedJs(js: String)(implicit ctx: Context): Html = embedJsUnsafe(js)
def embedJsUnsafe(js: String, nonce: Nonce): Frag = raw {
s"""<script nonce="$nonce">$js</script>"""
}
}

View File

@ -3,32 +3,34 @@ package templating
import chess.{ Color, Board, Pos }
import lila.api.Context
import play.twirl.api.Html
import lila.app.ui.ScalatagsTemplate._
import lila.game.Pov
trait ChessgroundHelper {
def chessground(board: Board, orient: Color, lastMove: List[Pos] = Nil)(implicit ctx: Context): Html = wrap {
if (ctx.pref.is3d) ""
else {
def top(p: Pos) = orient.fold(8 - p.y, p.y - 1) * 12.5
def left(p: Pos) = orient.fold(p.x - 1, 8 - p.x) * 12.5
val highlights = ctx.pref.highlight ?? lastMove.distinct.map { pos =>
s"""<square class="last-move" style="top:${top(pos)}%;left:${left(pos)}%"></square>"""
} mkString ""
val pieces =
if (ctx.pref.isBlindfold) ""
else board.pieces.map {
case (pos, piece) =>
val klass = s"${piece.color.name} ${piece.role.name}"
s"""<piece class="$klass" style="top:${top(pos)}%;left:${left(pos)}%"></piece>"""
def chessground(board: Board, orient: Color, lastMove: List[Pos] = Nil)(implicit ctx: Context): Frag = wrap {
raw {
if (ctx.pref.is3d) ""
else {
def top(p: Pos) = orient.fold(8 - p.y, p.y - 1) * 12.5
def left(p: Pos) = orient.fold(p.x - 1, 8 - p.x) * 12.5
val highlights = ctx.pref.highlight ?? lastMove.distinct.map { pos =>
s"""<square class="last-move" style="top:${top(pos)}%;left:${left(pos)}%"></square>"""
} mkString ""
s"$highlights$pieces"
val pieces =
if (ctx.pref.isBlindfold) ""
else board.pieces.map {
case (pos, piece) =>
val klass = s"${piece.color.name} ${piece.role.name}"
s"""<piece class="$klass" style="top:${top(pos)}%;left:${left(pos)}%"></piece>"""
} mkString ""
s"$highlights$pieces"
}
}
}
def chessground(pov: Pov)(implicit ctx: Context): Html = chessground(
def chessground(pov: Pov)(implicit ctx: Context): Frag = chessground(
board = pov.game.board,
orient = pov.color,
lastMove = pov.game.history.lastMove.map(_.origDest) ?? {
@ -36,9 +38,11 @@ trait ChessgroundHelper {
}
)
private def wrap(content: String) = Html {
s"""<div class="cg-board-wrap"><div class="cg-board">$content</div></div>"""
}
private def wrap(content: Frag): Frag = div(cls := "cg-board-wrap")(
div(cls := "cg-board")(content)
)
lazy val miniBoardContent = wrap("")
lazy val chessgroundSvg = wrap(raw("<svg></svg>"))
}

View File

@ -7,9 +7,9 @@ import scala.collection.mutable.AnyRefMap
import org.joda.time.format._
import org.joda.time.format.ISODateTimeFormat
import org.joda.time.{ Period, PeriodType, DurationFieldType, DateTime, DateTimeZone }
import play.twirl.api.Html
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
trait DateHelper { self: I18nHelper =>
@ -61,9 +61,8 @@ trait DateHelper { self: I18nHelper =>
def showEnglishDate(date: DateTime): String =
englishDateFormatter print date
def semanticDate(date: DateTime)(implicit ctx: Context) = Html {
s"""<time datetime="${isoDate(date)}">${showDate(date)}</time>"""
}
def semanticDate(date: DateTime)(implicit ctx: Context): Frag =
timeTag(datetimeAttr := isoDate(date))(showDate(date))
def showPeriod(period: Period)(implicit ctx: Context): String =
periodFormatter(ctx) print period.normalizedStandard(periodType)
@ -74,13 +73,15 @@ trait DateHelper { self: I18nHelper =>
def isoDate(date: DateTime): String = isoFormatter print date
private val oneDayMillis = 1000 * 60 * 60 * 24
def momentFromNow(date: DateTime, alwaysRelative: Boolean = false, once: Boolean = false) = Html {
def momentFromNow(date: DateTime, alwaysRelative: Boolean = false, once: Boolean = false): Frag = {
if (!alwaysRelative && (date.getMillis - nowMillis) > oneDayMillis) absClientDateTime(date)
s"""<time class="timeago${if (once) " once" else ""}" datetime="${isoDate(date)}"></time>"""
}
def absClientDateTime(date: DateTime) = Html {
s"""<time class="timeago abs" datetime="${isoDate(date)}"></time>"""
else timeTag(cls := s"timeago${once ?? " once"}", datetimeAttr := isoDate(date))
}
def absClientDateTime(date: DateTime): Frag =
timeTag(cls := "timeago abs", datetimeAttr := isoDate(date))("-")
def momentFromNowOnce(date: DateTime) = momentFromNow(date, once = true)
def secondsFromNow(seconds: Int, alwaysRelative: Boolean = false)(implicit ctx: Context) =

View File

@ -3,17 +3,13 @@ package templating
import scala.concurrent.duration._
import play.twirl.api.Html
import lila.api.Env.{ current => apiEnv }
import lila.app.ui.ScalatagsTemplate._
object Environment
extends lila.Lilaisms
with StringHelper
with HtmlHelper
with JsonHelper
with AssetHelper
with RequestHelper
with DateHelper
with NumberHelper
with PaginatorHelper
@ -27,9 +23,7 @@ object Environment
with SecurityHelper
with TeamHelper
with TournamentHelper
with SimulHelper
with ChessgroundHelper
with ui.ScalatagsTwirl {
with ChessgroundHelper {
type FormWithCaptcha = (play.api.data.Form[_], lila.common.Captcha)
@ -48,7 +42,7 @@ object Environment
def contactEmail = apiEnv.Net.Email
def contactEmailLink = Html(s"""<a href="mailto:$contactEmail">$contactEmail</a>""")
def contactEmailLink = a(href := s"mailto:$contactEmail")(contactEmail)
def cspEnabled = apiEnv.cspEnabledSetting.get _
@ -58,5 +52,7 @@ object Environment
def reportNbOpen: Int =
lila.report.Env.current.api.nbOpen.awaitOrElse(10.millis, 0)
def NotForKids(f: => Html)(implicit ctx: lila.api.Context) = if (ctx.kid) emptyHtml else f
def NotForKids(f: => Frag)(implicit ctx: lila.api.Context) = if (ctx.kid) emptyFrag else f
val spinner: Frag = raw("""<div class="spinner"><svg viewBox="0 0 40 40"><circle cx=20 cy=20 r=18 fill="none"></circle></svg></div>""")
}

View File

@ -2,33 +2,29 @@ package lila.app
package templating
import play.api.data._
import play.twirl.api.Html
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
import lila.i18n.I18nDb
trait FormHelper { self: I18nHelper =>
def errMsg(form: Field)(implicit ctx: Context): Html = errMsg(form.errors)
def errMsg(form: Field)(implicit ctx: Context): Frag = errMsg(form.errors)
def errMsg(form: Form[_])(implicit ctx: Context): Html = errMsg(form.errors)
def errMsg(form: Form[_])(implicit ctx: Context): Frag = errMsg(form.errors)
def errMsg(error: FormError)(implicit ctx: Context): Html = Html {
s"""<p class="error">${transKey(error.message, I18nDb.Site, error.args)}</p>"""
}
def errMsg(error: FormError)(implicit ctx: Context): Frag =
p(cls := "error")(transKey(error.message, I18nDb.Site, error.args))
def errMsg(errors: Seq[FormError])(implicit ctx: Context): Html = Html {
def errMsg(errors: Seq[FormError])(implicit ctx: Context): Frag =
errors map errMsg mkString
}
def globalError(form: Form[_])(implicit ctx: Context): Option[Html] =
def globalError(form: Form[_])(implicit ctx: Context): Option[Frag] =
form.globalError map errMsg
val booleanChoices = Seq("true" -> "✓ Yes", "false" -> "✗ No")
object form3 extends ui.ScalatagsPlay {
import ui.ScalatagsTemplate._
object form3 {
private val idPrefix = "form3"
@ -47,20 +43,14 @@ trait FormHelper { self: I18nHelper =>
* such as `optional(nonEmptyText)`.
* And we can't tell from the Field whether it's optional or not :(
*/
// case ("constraint.required", _) => required := true
// case ("constraint.required", _) => required
case ("constraint.minLength", Seq(m: Int)) => minlength := m
case ("constraint.maxLength", Seq(m: Int)) => maxlength := m
case ("constraint.min", Seq(m: Int)) => min := m
case ("constraint.max", Seq(m: Int)) => max := m
}
/* All public methods must return HTML
* because twirl just calls toString on scalatags frags
* and that escapes the content :( */
def split(html: Html): Html = div(cls := "form-split")(html)
def split(frags: Frag*): Html = div(cls := "form-split")(frags)
val split = div(cls := "form-split")
def group(
field: Field,
@ -68,94 +58,84 @@ trait FormHelper { self: I18nHelper =>
klass: String = "",
half: Boolean = false,
help: Option[Frag] = None
)(content: Field => Frag)(implicit ctx: Context): Html =
div(cls := List(
"form-group" -> true,
"is-invalid" -> field.hasErrors,
"form-half" -> half,
klass -> klass.nonEmpty
))(
groupLabel(field)(labelContent),
content(field),
errors(field),
help map { helper(_) }
)
)(content: Field => Frag)(implicit ctx: Context): Frag = div(cls := List(
"form-group" -> true,
"is-invalid" -> field.hasErrors,
"form-half" -> half,
klass -> klass.nonEmpty
))(
groupLabel(field)(labelContent),
content(field),
errors(field),
help map { helper(_) }
)
def input(field: Field, typ: String = "", klass: String = ""): BaseTagType =
st.input(
st.id := id(field),
name := field.name,
value := field.value,
`type` := typ.nonEmpty.option(typ),
tpe := typ.nonEmpty.option(typ),
cls := List("form-control" -> true, klass -> klass.nonEmpty)
)(validationModifiers(field))
def inputHtml(field: Field, typ: String = "", klass: String = "")(modifiers: Modifier*): Html =
input(field, typ, klass)(modifiers)
def checkbox(
field: Field,
labelContent: Frag,
half: Boolean = false,
help: Option[Frag] = None,
disabled: Boolean = false
): Html =
div(cls := List(
"form-check form-group" -> true,
"form-half" -> half
))(
div(
span(cls := "form-check-input")(
st.input(
st.id := id(field),
name := field.name,
value := "true",
`type` := "checkbox",
cls := "form-control cmn-toggle",
checked := field.value.has("true").option(true),
st.disabled := disabled.option(true)
),
label(`for` := id(field))
): Frag = div(cls := List(
"form-check form-group" -> true,
"form-half" -> half
))(
div(
span(cls := "form-check-input")(
st.input(
st.id := id(field),
name := field.name,
value := "true",
tpe := "checkbox",
cls := "form-control cmn-toggle",
field.value.has("true") option checked,
disabled option st.disabled
),
groupLabel(field)(labelContent)
label(`for` := id(field))
),
help map { helper(_) }
)
groupLabel(field)(labelContent)
),
help map { helper(_) }
)
def select(
field: Field,
options: Iterable[(Any, String)],
default: Option[String] = None
): Html =
st.select(
st.id := id(field),
name := field.name,
cls := "form-control"
)(validationModifiers(field))(
default map { option(value := "")(_) },
options.toSeq map {
case (value, name) => option(
st.value := value.toString,
selected := field.value.has(value.toString).option(true)
)(name)
}
)
): Frag = st.select(
st.id := id(field),
name := field.name,
cls := "form-control"
)(validationModifiers(field))(
default map { option(value := "")(_) },
options.toSeq map {
case (value, name) => option(
st.value := value.toString,
field.value.has(value.toString) option selected
)(name)
}
)
def textarea(
field: Field,
klass: String = ""
)(modifiers: Modifier*): Html =
st.textarea(
st.id := id(field),
name := field.name,
cls := List("form-control" -> true, klass -> klass.nonEmpty)
)(validationModifiers(field))(modifiers)(~field.value)
)(modifiers: Modifier*): Frag = st.textarea(
st.id := id(field),
name := field.name,
cls := List("form-control" -> true, klass -> klass.nonEmpty)
)(validationModifiers(field))(modifiers)(~field.value)
val actions = div(cls := "form-actions")
def actionsHtml(html: Frag): Html = actions(html)
val action = div(cls := "form-actions single")
def actionHtml(html: Frag): Html = div(cls := "form-actions single")(html)
def submit(
content: Frag,
@ -163,8 +143,8 @@ trait FormHelper { self: I18nHelper =>
nameValue: Option[(String, String)] = None,
klass: String = "",
confirm: Option[String] = None
): Html = button(
`type` := "submit",
): Frag = button(
tpe := "submit",
dataIcon := icon,
name := nameValue.map(_._1),
value := nameValue.map(_._2),
@ -177,34 +157,33 @@ trait FormHelper { self: I18nHelper =>
title := confirm
)(content)
def hidden(field: Field, value: Option[String] = None): Html =
st.input(
st.id := id(field),
name := field.name,
st.value := value.orElse(field.value),
`type` := "hidden"
)
def hidden(field: Field, value: Option[String] = None): Frag = st.input(
st.id := id(field),
name := field.name,
st.value := value.orElse(field.value),
tpe := "hidden"
)
def password(field: Field, content: Html)(implicit ctx: Context): Frag =
group(field, content)(input(_, typ = "password")(required := true))
def password(field: Field, content: Frag)(implicit ctx: Context): Frag =
group(field, content)(input(_, typ = "password")(required))
def passwordNoAutocomplete(field: Field, content: Html)(implicit ctx: Context): Frag =
group(field, content)(input(_, typ = "password")(autocomplete := "off")(required := true))
def passwordModified(field: Field, content: Frag)(modifiers: Modifier*)(implicit ctx: Context): Frag =
group(field, content)(input(_, typ = "password")(required)(modifiers))
def globalError(form: Form[_])(implicit ctx: Context): Option[Html] =
def globalError(form: Form[_])(implicit ctx: Context): Option[Frag] =
form.globalError map { err =>
div(cls := "form-group is-invalid")(error(err))
}
def flatpickr(field: Field, withTime: Boolean = true): Html =
def flatpickr(field: Field, withTime: Boolean = true): Frag =
input(field, klass = "flatpickr")(
dataEnableTime := withTime,
datatime24h := withTime
)
object file {
def image(name: String): Html = st.input(`type` := "file", st.name := name, accept := "image/*")
def pgn(name: String): Html = st.input(`type` := "file", st.name := name, accept := ".pgn")
def image(name: String): Frag = st.input(tpe := "file", st.name := name, accept := "image/*")
def pgn(name: String): Frag = st.input(tpe := "file", st.name := name, accept := ".pgn")
}
}
}

View File

@ -1,9 +1,8 @@
package lila.app
package templating
import play.twirl.api.Html
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
import lila.forum.Post
trait ForumHelper { self: UserHelper with StringHelper =>
@ -22,7 +21,7 @@ trait ForumHelper { self: UserHelper with StringHelper =>
def authorName(post: Post) = post.userId match {
case Some(userId) => userIdSpanMini(userId, withOnline = true)
case None => Html(lila.user.User.anonymous)
case None => frag(lila.user.User.anonymous)
}
def authorLink(
@ -30,9 +29,9 @@ trait ForumHelper { self: UserHelper with StringHelper =>
cssClass: Option[String] = None,
withOnline: Boolean = true,
modIcon: Boolean = false
) =
if (post.erased) Html(s"""<span class="author">${lila.common.String.erasedHtml}</span>""")
else post.userId.fold(Html(lila.user.User.anonymous)) { userId =>
): Frag =
if (post.erased) span(cls := "author")("<erased>")
else post.userId.fold(frag(lila.user.User.anonymous)) { userId =>
userIdLink(userId.some, cssClass = cssClass, withOnline = withOnline, modIcon = modIcon)
}
}

View File

@ -4,15 +4,18 @@ package templating
import chess.format.Forsyth
import chess.{ Status => S, Color, Clock, Mode }
import controllers.routes
import play.twirl.api.Html
import scalatags.Text.Frag
import lila.common.String.html.escapeHtml
import lila.app.ui.ScalatagsTemplate._
import lila.game.{ Game, Player, Namer, Pov }
import lila.i18n.{ I18nKeys, enLang }
import lila.user.{ User, UserContext }
import lila.i18n.{ I18nKeys => trans, enLang }
import lila.user.{ User, UserContext, Title }
trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHelper with HtmlHelper with ChessgroundHelper =>
trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHelper with ChessgroundHelper =>
private val dataLive = attr("data-live")
private val dataColor = attr("data-color")
private val dataFen = attr("data-fen")
private val dataLastmove = attr("data-lastmove")
def netBaseUrl: String
def cdnUrl(path: String): String
@ -67,34 +70,49 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
}
def variantName(variant: chess.variant.Variant)(implicit ctx: UserContext) = variant match {
case chess.variant.Standard => I18nKeys.standard.txt()
case chess.variant.FromPosition => I18nKeys.fromPosition.txt()
case chess.variant.Standard => trans.standard.txt()
case chess.variant.FromPosition => trans.fromPosition.txt()
case v => v.name
}
def variantNameNoCtx(variant: chess.variant.Variant) = variant match {
case chess.variant.Standard => I18nKeys.standard.literalTxtTo(enLang)
case chess.variant.FromPosition => I18nKeys.fromPosition.literalTxtTo(enLang)
case chess.variant.Standard => trans.standard.literalTxtTo(enLang)
case chess.variant.FromPosition => trans.fromPosition.literalTxtTo(enLang)
case v => v.name
}
def shortClockName(clock: Option[Clock.Config])(implicit ctx: UserContext): Html =
clock.fold(I18nKeys.unlimited())(shortClockName)
def shortClockName(clock: Option[Clock.Config])(implicit ctx: UserContext): Frag =
clock.fold[Frag](trans.unlimited())(shortClockName)
def shortClockName(clock: Clock.Config): Html = Html(clock.show)
def shortClockName(clock: Clock.Config): Frag = raw(clock.show)
def modeName(mode: Mode)(implicit ctx: UserContext): String = mode match {
case Mode.Casual => I18nKeys.casual.txt()
case Mode.Rated => I18nKeys.rated.txt()
case Mode.Casual => trans.casual.txt()
case Mode.Rated => trans.rated.txt()
}
def modeNameNoCtx(mode: Mode): String = mode match {
case Mode.Casual => I18nKeys.casual.literalTxtTo(enLang)
case Mode.Rated => I18nKeys.rated.literalTxtTo(enLang)
case Mode.Casual => trans.casual.literalTxtTo(enLang)
case Mode.Rated => trans.rated.literalTxtTo(enLang)
}
def playerUsername(player: Player, withRating: Boolean = true, withTitle: Boolean = true) =
Namer.player(player, withRating, withTitle)(lightUser)
def playerUsername(player: Player, withRating: Boolean = true, withTitle: Boolean = true): Frag =
player.aiLevel.fold[Frag](
player.userId.flatMap(lightUser).fold[Frag](lila.user.User.anonymous) { user =>
val title = user.title ifTrue withTitle map { t =>
frag(
span(
cls := "title",
(Title(t) == Title.BOT) option dataBotAttr,
st.title := Title titleName Title(t)
)(t),
" "
)
}
if (withRating) frag(title, user.name, " ", "(", lila.game.Namer ratingString player, ")")
else frag(title, user.name)
}
) { level => raw(s"A.I. level $level") }
def playerText(player: Player, withRating: Boolean = false) =
Namer.playerText(player, withRating)(lightUser)
@ -102,8 +120,8 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
def gameVsText(game: Game, withRatings: Boolean = false): String =
Namer.gameVsText(game, withRatings)(lightUser)
val berserkIconSpan = """<span data-icon="`"></span>"""
val statusIconSpan = """<span class="status"></span>"""
val berserkIconSpan = iconTag("`")
val statusIconSpan = i(cls := "status")
def playerLink(
player: Player,
@ -116,59 +134,63 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
withBerserk: Boolean = false,
mod: Boolean = false,
link: Boolean = true
)(implicit ctx: UserContext) = Html {
)(implicit ctx: UserContext): Frag = {
val statusIcon =
if (withStatus) statusIconSpan
else if (withBerserk && player.berserk) berserkIconSpan
else ""
if (withStatus) statusIconSpan.some
else if (withBerserk && player.berserk) berserkIconSpan.some
else none
player.userId.flatMap(lightUser) match {
case None =>
val klass = cssClass.??(" " + _)
val content = (player.aiLevel, player.name) match {
case (Some(level), _) => aiNameHtml(level, withRating).body
case (_, Some(name)) => escapeHtml(name).body
case _ => User.anonymous
}
s"""<span class="user_link$klass">$content$statusIcon</span>"""
case Some(user) =>
val klass = userClass(user.id, cssClass, withOnline)
val href = s"${routes.User show user.name}${if (mod) "?mod" else ""}"
val content = playerUsername(player, withRating)
val diff = (player.ratingDiff ifTrue withDiff) ?? showRatingDiff
val mark = engine ?? s"""<span class="engine_mark" title="${I18nKeys.thisPlayerUsesChessComputerAssistance()}"></span>"""
val icon = withOnline ?? lineIcon(user)
val space = if (withOnline) "&nbsp;" else ""
val tag = if (link) "a" else "span"
s"""<$tag $klass href="$href">$icon$space$content$diff$mark</$tag>$statusIcon"""
span(cls := s"user-link$klass")(
(player.aiLevel, player.name) match {
case (Some(level), _) => aiNameFrag(level, withRating)
case (_, Some(name)) => name
case _ => User.anonymous
},
statusIcon
)
case Some(user) => frag(
(if (link) a else span)(
cls := userClass(user.id, cssClass, withOnline),
href := s"${routes.User show user.name}${if (mod) "?mod" else ""}"
)(
withOnline option frag(lineIcon(user), " "),
playerUsername(player, withRating),
(player.ratingDiff ifTrue withDiff) map { d => frag(" ", showRatingDiff(d)) },
engine option span(cls := "engine_mark", title := trans.thisPlayerUsesChessComputerAssistance.txt())
),
statusIcon
)
}
}
def gameEndStatus(game: Game)(implicit ctx: UserContext): String = game.status match {
case S.Aborted => I18nKeys.gameAborted.txt()
case S.Mate => I18nKeys.checkmate.txt()
case S.Aborted => trans.gameAborted.txt()
case S.Mate => trans.checkmate.txt()
case S.Resign => game.loser match {
case Some(p) if p.color.white => I18nKeys.whiteResigned.txt()
case _ => I18nKeys.blackResigned.txt()
case Some(p) if p.color.white => trans.whiteResigned.txt()
case _ => trans.blackResigned.txt()
}
case S.UnknownFinish => I18nKeys.finished.txt()
case S.Stalemate => I18nKeys.stalemate.txt()
case S.UnknownFinish => trans.finished.txt()
case S.Stalemate => trans.stalemate.txt()
case S.Timeout => game.loser match {
case Some(p) if p.color.white => I18nKeys.whiteLeftTheGame.txt()
case Some(_) => I18nKeys.blackLeftTheGame.txt()
case None => I18nKeys.draw.txt()
case Some(p) if p.color.white => trans.whiteLeftTheGame.txt()
case Some(_) => trans.blackLeftTheGame.txt()
case None => trans.draw.txt()
}
case S.Draw => I18nKeys.draw.txt()
case S.Outoftime => I18nKeys.timeOut.txt()
case S.Draw => trans.draw.txt()
case S.Outoftime => trans.timeOut.txt()
case S.NoStart => {
val color = game.loser.fold(Color.white)(_.color).name.capitalize
s"$color didn't move"
}
case S.Cheat => "Cheat detected"
case S.VariantEnd => game.variant match {
case chess.variant.KingOfTheHill => I18nKeys.kingInTheCenter.txt()
case chess.variant.ThreeCheck => I18nKeys.threeChecks.txt()
case chess.variant.RacingKings => I18nKeys.raceFinished.txt()
case _ => I18nKeys.variantEnding.txt()
case chess.variant.KingOfTheHill => trans.kingInTheCenter.txt()
case chess.variant.ThreeCheck => trans.threeChecks.txt()
case chess.variant.RacingKings => trans.raceFinished.txt()
case _ => trans.variantEnding.txt()
}
case _ => ""
}
@ -203,6 +225,10 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
}
}.toString
def gameLink(pov: Pov)(implicit ctx: UserContext): String = gameLink(pov.game, pov.color)
private val cgBoard = div(cls := "cg-board")
def gameFen(
pov: Pov,
ownerLink: Boolean = false,
@ -210,34 +236,40 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
withTitle: Boolean = true,
withLink: Boolean = true,
withLive: Boolean = true
)(implicit ctx: UserContext) = Html {
)(implicit ctx: UserContext): Frag = {
val game = pov.game
val isLive = withLive && game.isBeingPlayed
val href = withLink ?? s"""href="${gameLink(game, pov.color, ownerLink, tv)}""""
val title = withTitle ?? s"""title="${gameTitle(game, pov.color)}""""
val cssClass = isLive ?? ("live live_" + game.id)
val live = isLive ?? game.id
val fen = Forsyth exportBoard game.board
val lastMove = ~game.lastMoveKeys
val cssClass = isLive ?? ("live mini-board-" + game.id)
val variant = game.variant.key
val tag = if (withLink) "a" else "span"
s"""<$tag $href $title class="mini_board mini_board_${game.id} parse_fen is2d $cssClass $variant" data-live="$live" data-color="${pov.color.name}" data-fen="$fen" data-lastmove="$lastMove">$miniBoardContent</$tag>"""
val tag = if (withLink) a else span
val classes = s"mini-board mini-board-${game.id} cg-board-wrap parse-fen is2d $cssClass $variant"
tag(
href := withLink.option(gameLink(game, pov.color, ownerLink, tv)),
title := withTitle.option(gameTitle(game, pov.color)),
cls := s"mini-board mini-board-${game.id} cg-board-wrap parse-fen is2d $cssClass $variant",
dataLive := isLive.option(game.id),
dataColor := pov.color.name,
dataFen := Forsyth.exportBoard(game.board),
dataLastmove := ~game.lastMoveKeys
)(cgBoard)
}
def gameFenNoCtx(pov: Pov, tv: Boolean = false, blank: Boolean = false) = Html {
def gameFenNoCtx(pov: Pov, tv: Boolean = false, blank: Boolean = false): Frag = {
val isLive = pov.game.isBeingPlayed
val variant = pov.game.variant.key
s"""<a href="%s%s" title="%s" class="mini_board mini_board_${pov.gameId} parse_fen is2d %s $variant" data-live="%s" data-color="%s" data-fen="%s" data-lastmove="%s"%s>$miniBoardContent</a>""".format(
blank ?? netBaseUrl,
if (tv) routes.Tv.index else routes.Round.watcher(pov.gameId, pov.color.name),
gameTitle(pov.game, pov.color),
isLive ?? ("live live_" + pov.gameId),
isLive ?? pov.gameId,
pov.color.name,
Forsyth exportBoard pov.game.board,
~pov.game.lastMoveKeys,
blank ?? """ target="_blank""""
)
a(
href := (if (tv) routes.Tv.index() else routes.Round.watcher(pov.gameId, pov.color.name)),
title := gameTitle(pov.game, pov.color),
cls := List(
s"mini-board mini-board-${pov.gameId} cg-board-wrap parse-fen is2d $variant" -> true,
s"live mini-board-${pov.gameId}" -> isLive
),
dataLive := isLive.option(pov.gameId),
dataColor := pov.color.name,
dataFen := Forsyth.exportBoard(pov.game.board),
dataLastmove := ~pov.game.lastMoveKeys,
target := blank.option("_blank")
)(cgBoard)
}
def challengeTitle(c: lila.challenge.Challenge)(implicit ctx: UserContext) = {

View File

@ -1,26 +0,0 @@
package lila.app
package templating
import ornicar.scalalib.Zero
import play.twirl.api.Html
trait HtmlHelper {
val emptyHtml = Html("")
implicit val LilaHtmlZero: Zero[Html] = Zero.instance(emptyHtml)
implicit val LilaHtmlMonoid = scalaz.Monoid.instance[Html](
(a, b) => Html(a.body + b.body),
LilaHtmlZero.zero
)
val spinner = Html("""<div class="spinner"><svg viewBox="0 0 40 40"><circle cx=20 cy=20 r=18 fill="none"></circle></svg></div>""")
@inline implicit def toPimpedHtml(html: Html) = new PimpedHtml(html)
}
final class PimpedHtml(private val self: Html) extends AnyVal {
def ++(other: Html): Html = Html(s"${self.body}${other.body}")
def ++(other: String): Html = Html(s"${self.body}${other}")
}

View File

@ -2,9 +2,9 @@ package lila.app
package templating
import play.api.libs.json.JsObject
import play.twirl.api.Html
import lila.common.Lang
import lila.app.ui.ScalatagsTemplate._
import lila.i18n.{ LangList, I18nKey, Translator, JsQuantity, I18nDb, JsDump, TimeagoLocales }
import lila.user.UserContext
@ -12,8 +12,8 @@ trait I18nHelper {
implicit def ctxLang(implicit ctx: UserContext): Lang = ctx.lang
def transKey(key: String, db: I18nDb.Ref, args: Seq[Any] = Nil)(implicit lang: Lang): Html =
Translator.html.literal(key, db, args, lang)
def transKey(key: String, db: I18nDb.Ref, args: Seq[Any] = Nil)(implicit lang: Lang): Frag =
Translator.frag.literal(key, db, args, lang)
def i18nJsObject(keys: Seq[I18nKey])(implicit lang: Lang): JsObject =
JsDump.keysToObject(keys, I18nDb.Site, lang)
@ -24,11 +24,10 @@ trait I18nHelper {
def i18nFullDbJsObject(db: I18nDb.Ref)(implicit lang: Lang): JsObject =
JsDump.dbToObject(db, lang)
private val defaultTimeagoLocale = TimeagoLocales.js.get("en") err "Missing en TimeagoLocales"
def timeagoLocaleScript(implicit ctx: lila.api.Context): String = {
TimeagoLocales.js.get(ctx.lang.code) orElse
TimeagoLocales.js.get(ctx.lang.language) getOrElse
defaultTimeagoLocale
~TimeagoLocales.js.get("en")
}
def langName = LangList.nameByStr _

View File

@ -1,25 +0,0 @@
package lila.app
package templating
import play.api.libs.json._
import play.twirl.api.Html
import scalatags.Text.{ Frag, RawFrag }
import lila.api.Context
trait JsonHelper {
def toJsonHtml[A: Writes](map: Map[Int, A]): Html = toJsonHtml {
map mapKeys (_.toString)
}
def toJsonHtml[A: Writes](a: A): Html = lila.common.String.html.safeJsonHtml(Json toJson a)
def toJsonFrag[A: Writes](a: A): Frag = RawFrag(lila.common.String.html.safeJsonValue(Json toJson a))
def htmlOrNull[A, B](a: Option[A])(f: A => Html) = a.fold(Html("null"))(f)
def jsOrNull[A: Writes](a: Option[A]) = a.fold(Html("null"))(x => toJsonHtml(x))
def jsUserIdString(implicit ctx: Context) = ctx.userId.fold("null")(id => s""""$id"""")
def jsUserId(implicit ctx: Context) = Html { jsUserIdString(ctx) }
}

View File

@ -1,9 +0,0 @@
package lila.app
package templating
import lila.api.Context
trait RequestHelper {
def currentPath(implicit ctx: Context) = ctx.req.path
}

View File

@ -1,8 +1,7 @@
package lila.app
package templating
import play.twirl.api.Html
import lila.app.ui.ScalatagsTemplate._
import lila.security.{ Permission, Granter }
import lila.user.{ User, UserContext }
@ -20,13 +19,11 @@ trait SecurityHelper {
def isGranted(permission: Permission, user: User): Boolean =
Granter(permission)(user)
def canGrant = Granter.canGrant _
def canViewRoles(user: User)(implicit ctx: UserContext): Boolean =
isGranted(_.ChangePermission) || (isGranted(_.Admin) && user.roles.nonEmpty)
def reportScore(score: lila.report.Report.Score) = Html {
s"""<div class="score ${score.color}" title="Report score">${score.value.toInt}</div>"""
}
// def reportScore(score: lila.report.Report.Score) = Html {
// s"""<div class="score"><i>Score</i><strong>${score.value.toInt}</strong></div>"""
// }
def reportScore(score: lila.report.Report.Score): Frag =
div(cls := s"score ${score.color}", title := "Report score")(score.value.toInt)
}

View File

@ -211,6 +211,12 @@ trait SetupHelper { self: I18nHelper =>
(Pref.InsightShare.EVERYBODY, trans.withEverybody.txt())
)
def translatedBoardResizeHandleChoices(implicit ctx: Context) = List(
(Pref.ResizeHandle.NEVER, trans.never.txt()),
(Pref.ResizeHandle.INITIAL, "Only on initial position"),
(Pref.ResizeHandle.ALWAYS, trans.always.txt())
)
def translatedBlindfoldChoices(implicit ctx: Context) = List(
Pref.Blindfold.NO -> trans.no.txt(),
Pref.Blindfold.YES -> trans.yes.txt()

View File

@ -1,15 +0,0 @@
package lila.app
package templating
import controllers.routes
import lila.simul.Simul
import play.twirl.api.Html
trait SimulHelper { self: I18nHelper =>
def simulLink(simulId: Simul.ID): Html = Html {
val url = routes.Simul.show(simulId)
s"""<a class="text" data-icon="|" href="$url">Simultaneous exhibition</a>"""
}
}

View File

@ -1,9 +1,8 @@
package lila.app
package templating
import play.twirl.api.Html
import lila.user.UserContext
import ui.ScalatagsTemplate._
trait StringHelper { self: NumberHelper =>
@ -15,10 +14,6 @@ trait StringHelper { self: NumberHelper =>
def pluralize(s: String, n: Int) = s"$n $s${if (n > 1) "s" else ""}"
def repositionTooltipUnsafe(link: Html, position: String) = Html {
link.body.replace("<a ", s"""<a data-pt-pos="$position" """)
}
def showNumber(n: Int): String = if (n > 0) s"+$n" else n.toString
implicit def lilaRichString(str: String) = new {
@ -30,23 +25,32 @@ trait StringHelper { self: NumberHelper =>
private val NumberFirstRegex = """(\d++)\s(.+)""".r
private val NumberLastRegex = """\s(\d++)$""".r.unanchored
def splitNumberUnsafe(s: String)(implicit ctx: UserContext): Html = Html {
s match {
case NumberFirstRegex(number, text) =>
s"<strong>${(~parseIntOption(number)).localize}</strong><br />$text"
case NumberLastRegex(n) if s.length > n.length + 1 =>
s"${s.dropRight(n.length + 1)}<br /><strong>${(~parseIntOption(n)).localize}</strong>"
case h => h.replaceIf('\n', "<br />")
def splitNumber(s: Frag)(implicit ctx: UserContext): Frag = {
val rendered = s.render
rendered match {
case NumberFirstRegex(number, html) => frag(
strong((~parseIntOption(number)).localize),
br,
raw(html)
)
case NumberLastRegex(n) if rendered.length > n.length + 1 => frag(
raw(rendered.dropRight(n.length + 1)),
br,
strong((~parseIntOption(n)).localize)
)
case h => raw(h.replaceIf('\n', "<br>"))
}
}
def splitNumber(s: Html)(implicit ctx: UserContext): Html = splitNumberUnsafe(s.body)
def encodeFen(fen: String) = lila.common.String.base64.encode(fen).reverse
def addQueryParameter(url: String, key: String, value: Any) =
if (url contains "?") s"$url&$key=$value" else s"$url?$key=$value"
def htmlList(htmls: List[Html], separator: String = ", ") = Html {
htmls mkString separator
def fragList(frags: List[Frag], separator: String = ", "): Frag = frags match {
case Nil => emptyFrag
case one :: Nil => one
case first :: rest => first :: rest.map { f => frag(separator, f) }
}
}

View File

@ -2,11 +2,10 @@ package lila.app
package templating
import controllers.routes
import play.twirl.api.Html
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
import lila.team.Env.{ current => teamEnv }
import lila.common.String.html.escapeHtml
trait TeamHelper {
@ -15,15 +14,12 @@ trait TeamHelper {
def myTeam(teamId: String)(implicit ctx: Context): Boolean =
ctx.me.??(me => api.syncBelongsTo(teamId, me.id))
def teamIdToName(id: String): Html = escapeHtml(api teamName id getOrElse id)
def teamIdToName(id: String): Frag = StringFrag(api.teamName(id).getOrElse(id))
def teamLink(id: String, withIcon: Boolean = true): Html = Html {
val href = routes.Team.show(id)
val content = teamIdToName(id)
val icon = if (withIcon) """ data-icon="f"""" else ""
val space = if (withIcon) "&nbsp;" else ""
s"""<a$icon href="$href">$space$content</a>"""
}
def teamLink(id: String, withIcon: Boolean = true): Frag = a(
href := routes.Team.show(id),
dataIcon := withIcon.option("f")
)(withIcon option nbsp, teamIdToName(id))
def teamForumUrl(id: String) = routes.ForumCateg.show("team-" + id)
}

View File

@ -2,12 +2,12 @@ package lila.app
package templating
import controllers.routes
import lila.app.ui.ScalatagsTemplate._
import lila.tournament.Env.{ current => tournamentEnv }
import lila.tournament.{ Tournament, System, Schedule }
import lila.user.{ User, UserContext }
import play.api.libs.json.Json
import play.twirl.api.Html
trait TournamentHelper { self: I18nHelper with DateHelper with UserHelper =>
@ -24,16 +24,17 @@ trait TournamentHelper { self: I18nHelper with DateHelper with UserHelper =>
}
}
def tournamentLink(tour: Tournament): Html = Html {
val cssClass = if (tour.isScheduled) "text is-gold" else "text"
val url = routes.Tournament.show(tour.id)
s"""<a data-icon="g" class="$cssClass" href="$url">${tour.fullName}</a>"""
}
def tournamentLink(tour: Tournament): Frag = a(
dataIcon := "g",
cls := (if (tour.isScheduled) "text is-gold" else "text"),
href := routes.Tournament.show(tour.id).url
)(tour.fullName)
def tournamentLink(tourId: String): Html = Html {
val url = routes.Tournament.show(tourId)
s"""<a class="text" data-icon="g" href="$url">${tournamentIdToName(tourId)}</a>"""
}
def tournamentLink(tourId: String): Frag = a(
dataIcon := "g",
cls := "text",
href := routes.Tournament.show(tourId).url
)(tournamentIdToName(tourId))
def tournamentIdToName(id: String) = tournamentEnv.cached name id getOrElse "Tournament"
@ -47,7 +48,7 @@ trait TournamentHelper { self: I18nHelper with DateHelper with UserHelper =>
) ::: lila.rating.PerfType.leaderboardable.map { pt =>
pt.name -> icon(pt.iconChar)
}
def apply(name: String) = Html {
def apply(name: String): Frag = raw {
replacements.foldLeft(name) {
case (n, (from, to)) => n.replace(from, to)
}

View File

@ -1,29 +1,22 @@
package lila.app
package templating
import play.twirl.api.Html
import controllers.routes
import mashup._
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
import lila.common.LightUser
import lila.i18n.I18nKeys
import lila.i18n.{ I18nKeys => trans }
import lila.rating.{ PerfType, Perf }
import lila.user.{ User, Title, UserContext }
trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with NumberHelper =>
trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
def showProgress(progress: Int, withTitle: Boolean = true) = Html {
val span = progress match {
case 0 => ""
case p if p > 0 => s"""<span class="positive" data-icon="N">$p</span>"""
case p if p < 0 => s"""<span class="negative" data-icon="M">${math.abs(p)}</span>"""
}
val title = if (withTitle) """ data-hint="Rating progression over the last twelve games"""" else ""
val klass = if (withTitle) "progress hint--bottom" else "progress"
s"""<span$title class="$klass">$span</span>"""
}
def ratingProgress(progress: Int) =
if (progress > 0) goodTag(cls := "rp")(progress)
else if (progress < 0) badTag(cls := "rp")(math.abs(progress))
else emptyFrag
val topBarSortedPerfTypes: List[PerfType] = List(
PerfType.Bullet,
@ -40,38 +33,37 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
PerfType.Crazyhouse
)
def showPerfRating(rating: Int, name: String, nb: Int, provisional: Boolean, icon: Char, klass: String)(implicit ctx: Context) = Html {
val title = s"$name rating over ${nb.localize} games"
val attr = if (klass == "title") "title" else "data-hint"
val number = if (nb > 0) s"$rating${if (provisional) "?" else ""}"
else "&nbsp;&nbsp;&nbsp;-"
s"""<span $attr="$title" class="$klass"><span data-icon="$icon">$number</span></span>"""
}
def showPerfRating(rating: Int, name: String, nb: Int, provisional: Boolean, icon: Char)(implicit ctx: Context): Frag =
span(
title := s"$name rating over ${nb.localize} games",
dataIcon := icon,
cls := "text"
)(
if (nb > 0) frag(rating, provisional option "?")
else frag(nbsp, nbsp, nbsp, "-")
)
def showPerfRating(perfType: PerfType, perf: Perf, klass: String)(implicit ctx: Context): Html =
showPerfRating(perf.intRating, perfType.name, perf.nb, perf.provisional, perfType.iconChar, klass)
def showPerfRating(perfType: PerfType, perf: Perf)(implicit ctx: Context): Frag =
showPerfRating(perf.intRating, perfType.name, perf.nb, perf.provisional, perfType.iconChar)
def showPerfRating(u: User, perfType: PerfType, klass: String = "hint--bottom")(implicit ctx: Context): Html =
showPerfRating(perfType, u perfs perfType, klass)
def showPerfRating(u: User, perfType: PerfType)(implicit ctx: Context): Frag =
showPerfRating(perfType, u perfs perfType)
def showPerfRating(u: User, perfKey: String)(implicit ctx: Context): Option[Html] =
def showPerfRating(u: User, perfKey: String)(implicit ctx: Context): Option[Frag] =
PerfType(perfKey) map { showPerfRating(u, _) }
def showBestPerf(u: User)(implicit ctx: Context): Option[Html] = u.perfs.bestPerf map {
case (pt, perf) => showPerfRating(pt, perf, klass = "hint--bottom")
def showBestPerf(u: User)(implicit ctx: Context): Option[Frag] = u.perfs.bestPerf map {
case (pt, perf) => showPerfRating(pt, perf)
}
def showBestPerfs(u: User, nb: Int)(implicit ctx: Context): Html = Html {
def showBestPerfs(u: User, nb: Int)(implicit ctx: Context): List[Frag] =
u.perfs.bestPerfs(nb) map {
case (pt, perf) => showPerfRating(pt, perf, klass = "hint--bottom").body
} mkString " "
}
def showRatingDiff(diff: Int) = Html {
diff match {
case 0 => """<span class="rp null">±0</span>"""
case d if d > 0 => s"""<span class="rp up">+$d</span>"""
case d => s"""<span class="rp down">${-d}</span>"""
case (pt, perf) => showPerfRating(pt, perf)
}
def showRatingDiff(diff: Int): Frag = diff match {
case 0 => span("±0")
case d if d > 0 => goodTag(s"+$d")
case d => badTag(s"${-d}")
}
def lightUser(userId: String): Option[LightUser] = Env.user lightUserSync userId
@ -94,8 +86,8 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
truncate: Option[Int] = None,
params: String = "",
modIcon: Boolean = false
): Html = Html {
userIdOption.flatMap(lightUser).fold(User.anonymous) { user =>
): Frag =
userIdOption.flatMap(lightUser).fold[Frag](User.anonymous) { user =>
userIdNameLink(
userId = user.id,
username = user.name,
@ -108,7 +100,6 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
modIcon = modIcon
)
}
}
def lightUserLink(
user: LightUser,
@ -117,31 +108,33 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
withTitle: Boolean = true,
truncate: Option[Int] = None,
params: String = ""
): Html = Html {
userIdNameLink(
userId = user.id,
username = user.name,
isPatron = user.isPatron,
title = withTitle ?? user.title map Title.apply,
cssClass = cssClass,
withOnline = withOnline,
truncate = truncate,
params = params,
modIcon = false
)
}
): Frag = userIdNameLink(
userId = user.id,
username = user.name,
isPatron = user.isPatron,
title = withTitle ?? user.title map Title.apply,
cssClass = cssClass,
withOnline = withOnline,
truncate = truncate,
params = params,
modIcon = false
)
def userIdLink(
userId: String,
cssClass: Option[String]
): Html = userIdLink(userId.some, cssClass)
): Frag = userIdLink(userId.some, cssClass)
def titleTag(title: Option[Title]) = Html {
title.fold("") { t =>
s"""<span class="title"${(t == Title.BOT) ?? " data-bot"} title="${Title titleName t}">$t</span>&nbsp;"""
}
def titleTag(title: Option[Title]): Option[Frag] = title map { t =>
frag(
span(
cls := s"title${(t == Title.BOT) ?? " data-bot"}",
st.title := Title.titleName(t)
)(t),
nbsp
)
}
def titleTag(lu: LightUser): Html = titleTag(lu.title map Title.apply)
def titleTag(lu: LightUser): Frag = titleTag(lu.title map Title.apply)
private def userIdNameLink(
userId: String,
@ -153,14 +146,14 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
title: Option[Title],
params: String,
modIcon: Boolean
): String = {
val klass = userClass(userId, cssClass, withOnline)
val href = userHref(username, params = params)
val content = truncate.fold(username)(username.take)
val titleS = titleTag(title).body
val icon = withOnline ?? (if (modIcon) moderatorIcon else lineIcon(isPatron))
s"""<a $klass $href>$icon$titleS$content</a>"""
}
): Frag = a(
cls := userClass(userId, cssClass, withOnline),
href := userUrl(username, params = params)
)(
withOnline ?? (if (modIcon) moderatorIcon else lineIcon(isPatron)),
titleTag(title),
truncate.fold(username)(username.take)
)
def userLink(
user: User,
@ -172,33 +165,15 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
withPerfRating: Option[PerfType] = None,
text: Option[String] = None,
params: String = ""
): Html = Html {
val klass = userClass(user.id, cssClass, withOnline, withPowerTip)
val href = userHref(user.username, params)
val content = text | user.username
val titleS = if (withTitle) titleTag(user.title).body else ""
val rating = userRating(user, withPerfRating, withBestRating)
val icon = withOnline ?? lineIcon(user)
s"""<a $klass $href>$icon$titleS$content$rating</a>"""
}
def userInfosLink(
userId: String,
rating: Option[Int],
cssClass: Option[String] = None,
withPowerTip: Boolean = true,
withTitle: Boolean = false,
withOnline: Boolean = true
) = {
val user = lightUser(userId)
val name = user.fold(userId)(_.name)
val klass = userClass(userId, cssClass, withOnline, withPowerTip)
val href = userHref(name)
val rat = rating ?? { r => s" ($r)" }
val titleS = withTitle ?? user ?? (u => titleTag(u).body)
val icon = withOnline ?? lineIcon(user)
Html(s"""<a $klass $href>$icon$titleS$name$rat</a>""")
}
): Frag = a(
cls := userClass(user.id, cssClass, withOnline, withPowerTip),
href := userUrl(user.username, params)
)(
withOnline ?? lineIcon(user),
withTitle option titleTag(user.title),
text | user.username,
userRating(user, withPerfRating, withBestRating)
)
def userSpan(
user: User,
@ -209,31 +184,37 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
withBestRating: Boolean = false,
withPerfRating: Option[PerfType] = None,
text: Option[String] = None
) = Html {
val klass = userClass(user.id, cssClass, withOnline, withPowerTip)
val href = s"data-${userHref(user.username)}"
val content = text | user.username
val titleS = if (withTitle) titleTag(user.title).body else ""
val rating = userRating(user, withPerfRating, withBestRating)
val icon = withOnline ?? lineIcon(user)
s"""<span $klass $href>$icon$titleS$content$rating</span>"""
}
): Frag = span(
cls := userClass(user.id, cssClass, withOnline, withPowerTip),
dataHref := userUrl(user.username)
)(
withOnline ?? lineIcon(user),
withTitle option titleTag(user.title),
text | user.username,
userRating(user, withPerfRating, withBestRating)
)
def userIdSpanMini(userId: String, withOnline: Boolean = false) = Html {
def userIdSpanMini(userId: String, withOnline: Boolean = false): Frag = {
val user = lightUser(userId)
val name = user.fold(userId)(_.name)
val content = user.fold(userId)(_.name)
val titleS = user.??(u => titleTag(u.title map Title.apply).body)
val klass = userClass(userId, none, withOnline)
val href = s"data-${userHref(name)}"
val icon = withOnline ?? lineIcon(user)
s"""<span $klass $href>$icon$titleS$content</span>"""
span(
cls := userClass(userId, none, withOnline),
dataHref := userUrl(name)
)(
withOnline ?? lineIcon(user),
user.??(u => titleTag(u.title map Title.apply)),
name
)
}
private def renderRating(perf: Perf) =
s"&nbsp;(${perf.intRating}${if (perf.provisional) "?" else ""})"
private def renderRating(perf: Perf): Frag = frag(
" (",
perf.intRating,
perf.provisional option "?",
")"
)
private def userRating(user: User, withPerfRating: Option[PerfType], withBestRating: Boolean) =
private def userRating(user: User, withPerfRating: Option[PerfType], withBestRating: Boolean): Frag =
withPerfRating match {
case Some(perfType) => renderRating(user.perfs(perfType))
case _ if withBestRating => user.perfs.bestPerf ?? {
@ -242,37 +223,36 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
case _ => ""
}
private def userHref(username: String, params: String = "") =
s"""href="${routes.User.show(username)}$params""""
private def addClass(cls: Option[String]) = cls.fold("")(" " + _)
private def userUrl(username: String, params: String = "") =
s"""${routes.User.show(username)}$params"""
protected def userClass(
userId: String,
cssClass: Option[String],
withOnline: Boolean,
withPowerTip: Boolean = true
): String = {
val online = if (withOnline) {
if (isOnline(userId)) " online" else " offline"
} else ""
s"""class="user_link${addClass(cssClass)}${addClass(withPowerTip option "ulpt")}$online""""
}
): List[(String, Boolean)] =
(withOnline ?? List((if (isOnline(userId)) "online" else "offline") -> true)) ::: List(
"user-link" -> true,
~cssClass -> cssClass.isDefined,
"ulpt" -> withPowerTip
)
def userGameFilterTitle(u: User, nbs: UserInfo.NbGames, filter: GameFilter)(implicit ctx: UserContext) =
splitNumber(userGameFilterTitleNoTag(u, nbs, filter))
def userGameFilterTitle(u: User, nbs: UserInfo.NbGames, filter: GameFilter)(implicit ctx: UserContext): Frag =
if (filter == GameFilter.Search) frag(br, trans.advancedSearch())
else splitNumber(userGameFilterTitleNoTag(u, nbs, filter))
def userGameFilterTitleNoTag(u: User, nbs: UserInfo.NbGames, filter: GameFilter)(implicit ctx: UserContext): Html = (filter match {
case GameFilter.All => I18nKeys.nbGames.pluralSame(u.count.game)
case GameFilter.Me => nbs.withMe ?? I18nKeys.nbGamesWithYou.pluralSame
case GameFilter.Rated => I18nKeys.nbRated.pluralSame(u.count.rated)
case GameFilter.Win => I18nKeys.nbWins.pluralSame(u.count.win)
case GameFilter.Loss => I18nKeys.nbLosses.pluralSame(u.count.loss)
case GameFilter.Draw => I18nKeys.nbDraws.pluralSame(u.count.draw)
case GameFilter.Playing => I18nKeys.nbPlaying.pluralSame(nbs.playing)
case GameFilter.Bookmark => I18nKeys.nbBookmarks.pluralSame(nbs.bookmark)
case GameFilter.Imported => I18nKeys.nbImportedGames.pluralSame(nbs.imported)
case GameFilter.Search => I18nKeys.advancedSearch()
def userGameFilterTitleNoTag(u: User, nbs: UserInfo.NbGames, filter: GameFilter)(implicit ctx: UserContext): String = (filter match {
case GameFilter.All => trans.nbGames.pluralSameTxt(u.count.game)
case GameFilter.Me => nbs.withMe ?? trans.nbGamesWithYou.pluralSameTxt
case GameFilter.Rated => trans.nbRated.pluralSameTxt(u.count.rated)
case GameFilter.Win => trans.nbWins.pluralSameTxt(u.count.win)
case GameFilter.Loss => trans.nbLosses.pluralSameTxt(u.count.loss)
case GameFilter.Draw => trans.nbDraws.pluralSameTxt(u.count.draw)
case GameFilter.Playing => trans.nbPlaying.pluralSameTxt(nbs.playing)
case GameFilter.Bookmark => trans.nbBookmarks.pluralSameTxt(nbs.bookmark)
case GameFilter.Imported => trans.nbImportedGames.pluralSameTxt(nbs.imported)
case GameFilter.Search => trans.advancedSearch.txt()
})
def describeUser(user: User) = {
@ -288,12 +268,12 @@ trait UserHelper { self: I18nHelper with StringHelper with HtmlHelper with Numbe
val patronIconChar = ""
val lineIconChar = ""
val lineIcon: String = """<i class="line"></i>"""
val patronIcon: String = """<i class="line patron" title="lichess Patron"></i>"""
val moderatorIcon: String = """<i class="line moderator" title="lichess Moderator"></i>"""
private def lineIcon(patron: Boolean): String = if (patron) patronIcon else lineIcon
private def lineIcon(user: Option[LightUser]): String = lineIcon(user.??(_.isPatron))
def lineIcon(user: LightUser): String = lineIcon(user.isPatron)
def lineIcon(user: User): String = lineIcon(user.isPatron)
def lineIconChar(user: User): String = if (user.isPatron) patronIconChar else lineIconChar
val lineIcon: Frag = i(cls := "line")
val patronIcon: Frag = i(cls := "line patron", title := "Lichess Patron")
val moderatorIcon: Frag = i(cls := "line moderator", title := "Lichess Mod")
private def lineIcon(patron: Boolean): Frag = if (patron) patronIcon else lineIcon
private def lineIcon(user: Option[LightUser]): Frag = lineIcon(user.??(_.isPatron))
def lineIcon(user: LightUser): Frag = lineIcon(user.isPatron)
def lineIcon(user: User): Frag = lineIcon(user.isPatron)
def lineIconChar(user: User): Frag = if (user.isPatron) patronIconChar else lineIconChar
}

View File

@ -0,0 +1,27 @@
package lila.app
package ui
import play.api.mvc.RequestHeader
import lila.common.{ Nonce, Lang }
case class EmbedConfig(bg: String, board: String, lang: Lang, req: RequestHeader, nonce: Nonce)
object EmbedConfig {
object implicits {
implicit def configLang(implicit config: EmbedConfig): Lang = config.lang
implicit def configReq(implicit config: EmbedConfig): RequestHeader = config.req
}
def apply(req: RequestHeader): EmbedConfig = EmbedConfig(
bg = get("bg", req).filterNot("auto" ==) | "light",
board = lila.pref.Theme(~get("theme", req)).cssClass,
lang = lila.i18n.I18nLangPicker(req, none),
req = req,
nonce = Nonce.random
)
private def get(name: String, req: RequestHeader): Option[String] =
req.queryString get name flatMap (_.headOption) filter (_.nonEmpty)
}

View File

@ -1,7 +1,7 @@
package lila.app
package ui
import lila.common.String.html.escapeHtml
import lila.app.ui.ScalatagsTemplate._
case class OpenGraph(
title: String,
@ -13,39 +13,45 @@ case class OpenGraph(
more: List[(String, String)] = Nil
) {
def frag = scalatags.Text.RawFrag(s"${og.str}${twitter.str}")
def frags: List[Frag] = og.frags ::: twitter.frags
object og {
private def tag(name: String, value: String) =
s"""<meta property="og:$name" content="${escapeHtml(value)}"/>"""
private val property = attr("property")
private def tag(name: String, value: String) = meta(
property := s"og:$name",
content := value
)
private val tupledTag = (tag _).tupled
def str = List(
def frags: List[Frag] = List(
"title" -> title,
"description" -> description,
"url" -> url,
"type" -> `type`,
"site_name" -> siteName
).map(tupledTag).mkString +
image.?? { tag("image", _) } +
more.map(tupledTag).mkString
).map(tupledTag) :::
image.map { tag("image", _) }.toList :::
more.map(tupledTag)
}
object twitter {
private def tag(name: String, value: String) =
s"""<meta name="twitter:$name" content="${escapeHtml(value)}"/>"""
private def tag(name: String, value: String) = meta(
st.name := s"twitter:$name",
content := value
)
private val tupledTag = (tag _).tupled
def str = List(
def frags: List[Frag] = List(
"card" -> "summary",
"title" -> title,
"description" -> description
).map(tupledTag).mkString +
image.?? { tag("image", _) } +
more.map(tupledTag).mkString
).map(tupledTag) :::
image.map { tag("image", _) }.toList :::
more.map(tupledTag)
}
}

View File

@ -3,24 +3,30 @@ package ui
import ornicar.scalalib.Zero
import play.twirl.api.Html
import scalatags.Text.all.{ genericAttr, attr, StringFrag }
import scalatags.text.Builder
import scalatags.Text.{ Frag, RawFrag, Attr, AttrValue, Modifier, Cap, Aggregate, Attrs, Styles }
import scalatags.Text.{ Aggregate, Cap }
import scalatags.Text.all._
// collection of lila attrs
trait ScalatagsAttrs {
lazy val minlength = attr("minlength") // missing from scalatags atm
lazy val dataTag = attr("data-tag")
lazy val dataIcon = attr("data-icon")
lazy val dataHint = attr("data-hint")
lazy val dataHref = attr("data-href")
lazy val dataCount = attr("data-count")
lazy val dataEnableTime = attr("data-enable-time")
lazy val datatime24h = attr("data-time_24h")
lazy val dataColor = attr("data-color")
lazy val dataFen = attr("data-fen")
lazy val novalidate = attr("novalidate")
val minlength = attr("minlength") // missing from scalatags atm
val dataTag = attr("data-tag")
val dataIcon = attr("data-icon")
val dataHref = attr("data-href")
val dataCount = attr("data-count")
val dataEnableTime = attr("data-enable-time")
val datatime24h = attr("data-time_24h")
val dataColor = attr("data-color")
val dataFen = attr("data-fen")
val dataRel = attr("data-rel")
val novalidate = attr("novalidate").empty
val datetimeAttr = attr("datetime")
val dataBotAttr = attr("data-bot").empty
val deferAttr = attr("defer").empty
object frame {
val scrolling = attr("scrolling")
val allowfullscreen = attr("allowfullscreen").empty
}
}
// collection of lila snippets
@ -30,15 +36,30 @@ trait ScalatagsSnippets extends Cap {
import scalatags.Text.all._
val nbsp = raw("&nbsp;")
val amp = raw("&amp;")
def iconTag(icon: Char): Tag = iconTag(icon.toString)
def iconTag(icon: String): Tag = i(dataIcon := icon)
def iconTag(icon: Char, text: Frag): Tag = iconTag(icon.toString, text)
def iconTag(icon: String, text: Frag): Tag = i(dataIcon := icon, cls := "text")(text)
lazy val dataBotAttr = attr("data-bot").empty
val styleTag = tag("style")(tpe := "text/css")
val ratingTag = tag("rating")
val countTag = tag("count")
val goodTag = tag("good")
val badTag = tag("bad")
val timeTag = tag("time")
def dataBot(title: lila.user.Title): Modifier =
if (title == lila.user.Title.BOT) dataBotAttr
else emptyModifier
def pagerNext(pager: lila.common.paginator.Paginator[_], url: Int => String): Option[Frag] =
pager.nextPage.map { np =>
div(cls := "pager none")(a(rel := "next", href := url(np))("Next"))
}
def pagerNextTable(pager: lila.common.paginator.Paginator[_], url: Int => String): Option[Frag] =
pager.nextPage.map { np =>
tr(th(cls := "pager none")(a(rel := "next", href := url(np))("Next")))
}
}
// basic imports from scalatags
@ -51,9 +72,16 @@ trait ScalatagsBundle extends Cap
// short prefix
trait ScalatagsPrefix {
object st extends Cap with Attrs with scalatags.text.Tags {
lazy val group = tag("group")
}
val group = tag("group")
val headTitle = tag("title")
val nav = tag("nav")
val section = tag("section")
val article = tag("article")
val aside = tag("aside")
val rating = tag("rating")
val frameborder = attr("frameborder")
}
}
// what to import in a pure scalatags template
@ -65,42 +93,24 @@ trait ScalatagsTemplate extends Styles
with ScalatagsPrefix {
val trans = lila.i18n.I18nKeys
def main = scalatags.Text.tags2.main
/* Convert play URLs to scalatags attributes with toString */
implicit val playCallAttr = genericAttr[play.api.mvc.Call]
}
object ScalatagsTemplate extends ScalatagsTemplate
// what to import in all twirl templates
trait ScalatagsTwirl extends ScalatagsPlay
// what to import in twirl templates containing scalatags forms
// Allows `*.rows := 5`
trait ScalatagsTwirlForm extends ScalatagsPlay with Cap with Aggregate {
object * extends Cap with Attrs with ScalatagsAttrs
}
object ScalatagsTwirlForm extends ScalatagsTwirlForm
// interop with play
trait ScalatagsPlay {
/* Feed frags back to twirl by converting them to rendered Html */
implicit def fragToPlayHtml(frag: Frag): Html = Html(frag.render)
/* Use play Html inside tags without double-encoding */
implicit def playHtmlToFrag(html: Html): Frag = RawFrag(html.body)
/* Convert play URLs to scalatags attributes with toString */
implicit val playCallAttr = genericAttr[play.api.mvc.Call]
@inline implicit def fragToHtml(frag: Frag) = new FragToHtml(frag)
}
final class FragToHtml(private val self: Frag) extends AnyVal {
def toHtml: Html = Html(self.render)
}
// generic extensions
trait ScalatagsExtensions {
implicit def stringValueFrag(sv: StringValue): Frag = new StringFrag(sv.value)
implicit val stringValueAttr = new AttrValue[StringValue] {
def apply(t: scalatags.text.Builder, a: Attr, v: StringValue): Unit =
t.setAttr(a.name, scalatags.text.Builder.GenericAttrValueSource(v.value))
}
implicit val charAttr = genericAttr[Char]
implicit val optionStringAttr = new AttrValue[Option[String]] {
@ -111,11 +121,6 @@ trait ScalatagsExtensions {
}
}
implicit val optionBooleanAttr = new AttrValue[Option[Boolean]] {
def apply(t: scalatags.text.Builder, a: Attr, v: Option[Boolean]): Unit =
if (~v) t.setAttr(a.name, scalatags.text.Builder.GenericAttrValueSource("true"))
}
/* for class maps such as List("foo" -> true, "active" -> isActive) */
implicit val classesAttr = new AttrValue[List[(String, Boolean)]] {
def apply(t: scalatags.text.Builder, a: Attr, m: List[(String, Boolean)]): Unit = {

View File

@ -8,10 +8,10 @@ import lila.pref.PrefCateg
object bits {
def categName(categ: lila.pref.PrefCateg)(implicit ctx: Context) = categ match {
case PrefCateg.GameDisplay => trans.gameDisplay()
case PrefCateg.ChessClock => trans.chessClock()
case PrefCateg.GameBehavior => trans.gameBehavior()
case PrefCateg.Privacy => trans.privacy()
def categName(categ: lila.pref.PrefCateg)(implicit ctx: Context): String = categ match {
case PrefCateg.GameDisplay => trans.gameDisplay.txt()
case PrefCateg.ChessClock => trans.chessClock.txt()
case PrefCateg.GameBehavior => trans.gameBehavior.txt()
case PrefCateg.Privacy => trans.privacy.txt()
}
}

View File

@ -11,26 +11,24 @@ object close {
def apply(u: lila.user.User, form: play.api.data.Form[_])(implicit ctx: Context) = account.layout(
title = s"${u.username} - ${trans.closeAccount.txt()}",
active = "close",
evenMoreCss = cssTag("form3.css")
active = "close"
) {
div(cls := "content_box small_box")(
div(cls := "signup_box")(
h1(dataIcon := "j", cls := "lichess_title text")(trans.closeAccount.frag()),
st.form(cls := "form3", action := routes.Account.closeConfirm, method := "POST")(
div(cls := "form-group")(trans.closeAccountExplanation.frag()),
div(cls := "form-group")("You will not be allowed to open a new account with the same name, even if the case if different."),
form3.passwordNoAutocomplete(form("passwd"), trans.password.frag()),
form3.actions(frag(
a(href := routes.User.show(u.username))(trans.changedMindDoNotCloseAccount.frag()),
form3.submit(
trans.closeAccount.frag(),
icon = "j".some,
confirm = "Closing is definitive. There is no going back. Are you sure?".some
)
))
div(cls := "account box box-pad")(
h1(dataIcon := "j", cls := "text")(trans.closeAccount()),
st.form(cls := "form3", action := routes.Account.closeConfirm, method := "POST")(
div(cls := "form-group")(trans.closeAccountExplanation()),
div(cls := "form-group")("You will not be allowed to open a new account with the same name, even if the case if different."),
form3.passwordModified(form("passwd"), trans.password())(autocomplete := "off"),
form3.actions(frag(
a(href := routes.User.show(u.username))(trans.changedMindDoNotCloseAccount()),
form3.submit(
trans.closeAccount(),
icon = "j".some,
confirm = "Closing is definitive. There is no going back. Are you sure?".some,
klass = "button-red"
)
)
))
)
}
)
}
}

View File

@ -0,0 +1,29 @@
package views.html
package account
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import controllers.routes
object email {
def apply(u: lila.user.User, form: play.api.data.Form[_])(implicit ctx: Context) = account.layout(
title = trans.changeEmail.txt(),
active = "email"
) {
div(cls := "account box box-pad")(
h1(
trans.changeEmail(),
ctx.req.queryString.contains("ok") option
frag(" ", i(cls := "is-green", dataIcon := "E"))
),
st.form(cls := "form3", action := routes.Account.emailApply, method := "POST")(
form3.password(form("passwd"), trans.password()),
form3.group(form("email"), trans.email())(form3.input(_, typ = "email")(required)),
form3.action(form3.submit(trans.apply()))
)
)
}
}

View File

@ -1,18 +0,0 @@
@(u: User, form: Form[_])(implicit ctx: Context)
@import lila.app.ui.ScalatagsTwirlForm._
@account.layout(title = s"${u.username} - ${trans.changeEmail.txt()}", active = "email", evenMoreCss = cssTag("form3.css")) {
<div class="content_box small_box">
<div class="signup_box">
<h1 class="lichess_title">
@trans.changeEmail()
@if(ctx.req.queryString.contains("ok")) { <span class="is-green" data-icon="E"></span>}
</h1>
<form class="form3" action="@routes.Account.emailApply" method="POST">
@form3.password(form("passwd"), trans.password.frag()).toHtml
@form3.group(form("email"), trans.email.frag())(form3.input(_, typ = "email"))
@form3.actionHtml(form3.submit(trans.apply.frag()))
</form>
</div>
</div>
}.toHtml

View File

@ -16,23 +16,23 @@ object emailConfirmHelp {
def apply(form: Form[_], status: Option[Status])(implicit ctx: Context) = views.html.base.layout(
title = title,
moreCss = cssTags("form3.css", "emailConfirmHelp.css"),
moreCss = cssTag("email-confirm"),
moreJs = jsTag("emailConfirmHelp.js")
)(frag(
div(cls := "content_box small_box emailConfirmHelp")(
h1(cls := "lichess_title")(title),
main(cls := "page-small box box-pad email-confirm-help")(
h1(title),
p("You signed up, but didn't receive your confirmation email?"),
st.form(cls := "form3", action := routes.Account.emailConfirmHelp, method := "get")(
form3.split(
form3.group(
form("username"),
trans.username.frag(),
trans.username(),
help = raw("What username did you create?").some
) { f =>
form3.input(f)(pattern := lila.user.User.newUsernameRegex.regex)
},
div(cls := "form-group")(
form3.submit(trans.apply.frag())
form3.submit(trans.apply())
)
)
),

View File

@ -13,20 +13,25 @@ object kid {
title = s"${u.username} - ${trans.kidMode.txt()}",
active = "kid"
) {
div(cls := "content_box small_box high")(
div(cls := "signup_box")(
h1(cls := "lichess_title")(trans.kidMode.frag()),
p(cls := "explanation")(trans.kidModeExplanation.frag()),
br,
br,
br,
st.form(action := s"${routes.Account.kidPost}?v=${!u.kid}", method := "POST")(
input(tpe := "submit", cls := "submit button", value := (if (u.kid) { trans.disableKidMode.txt() } else { trans.enableKidMode.txt() }))
),
br,
br,
p(trans.inKidModeTheLichessLogoGetsIconX.frag(raw(s"""<span title="${trans.kidMode()}" class="kiddo">😊</span>""")))
)
div(cls := "account box box-pad")(
h1(trans.kidMode()),
p(trans.kidModeExplanation()),
br,
br,
br,
st.form(action := s"${routes.Account.kidPost}?v=${!u.kid}", method := "POST")(
input(
tpe := "submit",
cls := List(
"button" -> true,
"button-red" -> u.kid
),
value := (if (u.kid) { trans.disableKidMode.txt() } else { trans.enableKidMode.txt() })
)
),
br,
br,
p(trans.inKidModeTheLichessLogoGetsIconX(span(cls := "kiddo", title := trans.kidMode.txt())(":)")))
)
}
}

View File

@ -1,7 +1,4 @@
package views.html
package account
import play.twirl.api.Html
package views.html.account
import lila.api.Context
import lila.app.templating.Environment._
@ -14,53 +11,58 @@ object layout {
def apply(
title: String,
active: String,
evenMoreCss: Html = emptyHtml,
evenMoreJs: Html = emptyHtml
)(body: Html)(implicit ctx: Context) = views.html.base.layout(
evenMoreCss: Frag = emptyFrag,
evenMoreJs: Frag = emptyFrag
)(body: Frag)(implicit ctx: Context): Frag = views.html.base.layout(
title = title,
menu = Some(frag(
lila.pref.PrefCateg.all.map { categ =>
a(cls := active.activeO(categ.slug), href := routes.Pref.form(categ.slug))(
bits.categName(categ)
)
},
a(cls := active.activeO("kid"), href := routes.Account.kid())(
trans.kidMode.frag()
),
div(cls := "sep"),
a(cls := active.activeO("editProfile"), href := routes.Account.profile())(
trans.editProfile.frag()
),
isGranted(_.Coach) option a(href := routes.Coach.edit)("Coach profile"),
div(cls := "sep"),
a(cls := active.activeO("username"), href := routes.Account.username())(
trans.changeUsername.frag()
),
a(cls := active.activeO("password"), href := routes.Account.passwd())(
trans.changePassword.frag()
),
a(cls := active.activeO("email"), href := routes.Account.email())(
trans.changeEmail.frag()
),
a(cls := active.activeO("twofactor"), href := routes.Account.twoFactor())(
"Two-factor authentication"
),
a(cls := active.activeO("security"), href := routes.Account.security())(
trans.security.frag()
),
div(cls := "sep"),
a(href := routes.Plan.index, style := "color: #d59120; font-weight: bold;")("Patron"),
div(cls := "sep"),
a(cls := active.activeO("oauth.token"), href := routes.OAuthToken.index)(
"API Access tokens"
),
ctx.noBot option a(cls := active.activeO("oauth.app"), href := routes.OAuthApp.index)("OAuth Apps"),
div(cls := "sep"),
a(cls := active.activeO("close"), href := routes.Account.close())(
trans.closeAccount.frag()
)
)),
moreCss = frag(cssTag("account.css"), evenMoreCss),
moreCss = frag(cssTag("account"), evenMoreCss),
moreJs = frag(jsTag("account.js"), evenMoreJs)
)(body)
) {
def activeCls(c: String) = cls := active.activeO(c)
main(cls := "account page-menu")(
st.nav(cls := "page-menu__menu subnav")(
lila.pref.PrefCateg.all.map { categ =>
a(activeCls(categ.slug), href := routes.Pref.form(categ.slug))(
bits.categName(categ)
)
},
a(activeCls("kid"), href := routes.Account.kid())(
trans.kidMode()
),
div(cls := "sep"),
a(activeCls("editProfile"), href := routes.Account.profile())(
trans.editProfile()
),
isGranted(_.Coach) option a(activeCls("coach"), href := routes.Coach.edit)("Coach profile"),
div(cls := "sep"),
a(activeCls("password"), href := routes.Account.passwd())(
trans.changePassword()
),
a(activeCls("email"), href := routes.Account.email())(
trans.changeEmail()
),
a(activeCls("username"), href := routes.Account.username())(
trans.changeUsername()
),
a(activeCls("twofactor"), href := routes.Account.twoFactor())(
"Two-factor authentication"
),
a(activeCls("security"), href := routes.Account.security())(
trans.security()
),
div(cls := "sep"),
a(href := routes.Plan.index)("Patron"),
div(cls := "sep"),
a(activeCls("oauth.token"), href := routes.OAuthToken.index)(
"API Access tokens"
),
ctx.noBot option a(activeCls("oauth.app"), href := routes.OAuthApp.index)("OAuth Apps"),
div(cls := "sep"),
a(activeCls("close"), href := routes.Account.close())(
trans.closeAccount()
)
),
div(cls := "page-menu__content")(body)
)
}
}

View File

@ -11,21 +11,19 @@ object passwd {
def apply(form: play.api.data.Form[_])(implicit ctx: Context) = account.layout(
title = trans.changePassword.txt(),
active = "password",
evenMoreCss = cssTag("form3.css")
active = "password"
) {
div(cls := "content_box small_box")(
div(cls := "signup_box")(
h1(cls := "lichess_title")(
trans.changePassword.frag(),
raw(ctx.req.queryString.contains("ok") ?? """ <span class="is-green" data-icon="E"></span>""")
),
st.form(cls := "form3", action := routes.Account.passwdApply, method := "POST")(
form3.password(form("oldPasswd"), trans.currentPassword.frag()),
form3.password(form("newPasswd1"), trans.newPassword.frag()),
form3.password(form("newPasswd2"), trans.newPasswordAgain.frag()),
form3.actionHtml(form3.submit(trans.apply.frag()))
)
div(cls := "account box box-pad")(
h1(
trans.changePassword(),
ctx.req.queryString.contains("ok") option
frag(" ", i(cls := "is-green", dataIcon := "E"))
),
st.form(cls := "form3", action := routes.Account.passwdApply, method := "POST")(
form3.password(form("oldPasswd"), trans.currentPassword()),
form3.password(form("newPasswd1"), trans.newPassword()),
form3.password(form("newPasswd2"), trans.newPasswordAgain()),
form3.action(form3.submit(trans.apply()))
)
)
}

View File

@ -10,10 +10,10 @@ import controllers.routes
object pref {
private def categFieldset(categ: lila.pref.PrefCateg, active: lila.pref.PrefCateg)(body: Frag) =
div(cls := List("none" -> (categ != active)))(body)
private def categFieldset(categ: lila.pref.PrefCateg, active: lila.pref.PrefCateg) =
div(cls := List("none" -> (categ != active)))
private def setting(name: Frag, body: Frag) = li(h2(name), body)
private def setting(name: Frag, body: Frag) = st.section(h2(name), body)
private def radios(field: play.api.data.Field, options: Iterable[(Any, String)], prefix: String = "ir") =
st.group(cls := "radio")(
@ -23,7 +23,7 @@ object pref {
div(
input(
st.id := s"$prefix$id",
st.checked := checked option true,
checked option st.checked,
cls := checked option "active",
`type` := "radio",
value := v._1.toString,
@ -36,137 +36,130 @@ object pref {
def apply(u: lila.user.User, form: play.api.data.Form[_], categ: lila.pref.PrefCateg)(implicit ctx: Context) = account.layout(
title = s"${bits.categName(categ)} - ${u.username} - ${trans.preferences.txt()}",
active = categ.slug,
evenMoreCss = cssTag("pref.css")
active = categ.slug
) {
val booleanChoices = Seq(0 -> trans.no.txt(), 1 -> trans.yes.txt())
div(cls := "content_box small_box prefs")(
div(cls := "signup_box")(
h1(cls := "lichess_title text", dataIcon := "%")(bits.categName(categ)),
st.form(cls := "autosubmit", action := routes.Pref.formApply, method := "POST")(
categFieldset(PrefCateg.GameDisplay, categ) {
ul(
setting(
trans.pieceAnimation.frag(),
radios(form("display.animation"), translatedAnimationChoices)
),
setting(
trans.materialDifference.frag(),
radios(form("display.captured"), booleanChoices)
),
setting(
trans.boardHighlights.frag(),
radios(form("display.highlight"), booleanChoices)
),
setting(
trans.pieceDestinations.frag(),
radios(form("display.destination"), booleanChoices)
),
setting(
trans.boardCoordinates.frag(),
radios(form("display.coords"), translatedBoardCoordinateChoices)
),
setting(
trans.moveListWhilePlaying.frag(),
radios(form("display.replay"), translatedMoveListWhilePlayingChoices)
),
setting(
trans.pgnPieceNotation.frag(),
radios(form("display.pieceNotation"), translatedPieceNotationChoices)
),
setting(
trans.zenMode.frag(),
radios(form("display.zen"), booleanChoices)
),
setting(
trans.blindfoldChess.frag(),
radios(form("display.blindfold"), translatedBlindfoldChoices)
)
)
},
categFieldset(PrefCateg.ChessClock, categ) {
ul(
setting(
trans.tenthsOfSeconds.frag(),
radios(form("clockTenths"), translatedClockTenthsChoices)
),
setting(
trans.horizontalGreenProgressBars.frag(),
radios(form("clockBar"), booleanChoices)
),
setting(
trans.soundWhenTimeGetsCritical.frag(),
radios(form("clockSound"), booleanChoices)
)
)
},
categFieldset(PrefCateg.GameBehavior, categ) {
ul(
setting(
trans.howDoYouMovePieces.frag(),
radios(form("behavior.moveEvent"), translatedMoveEventChoices)
),
setting(
trans.premovesPlayingDuringOpponentTurn.frag(),
radios(form("behavior.premove"), booleanChoices)
),
setting(
trans.takebacksWithOpponentApproval.frag(),
radios(form("behavior.takeback"), translatedTakebackChoices)
),
setting(
trans.promoteToQueenAutomatically.frag(),
radios(form("behavior.autoQueen"), translatedAutoQueenChoices)
),
setting(
trans.claimDrawOnThreefoldRepetitionAutomatically.frag(),
radios(form("behavior.autoThreefold"), translatedAutoThreefoldChoices)
),
setting(
trans.moveConfirmation.frag(),
radios(form("behavior.submitMove"), submitMoveChoices)
),
setting(
trans.confirmResignationAndDrawOffers.frag(),
radios(form("behavior.confirmResign"), confirmResignChoices)
),
setting(
trans.inputMovesWithTheKeyboard.frag(),
radios(form("behavior.keyboardMove"), booleanChoices)
),
setting(
trans.castleByMovingTheKingTwoSquaresOrOntoTheRook.frag(),
radios(form("behavior.rookCastle"), translatedRookCastleChoices)
)
)
},
categFieldset(PrefCateg.Privacy, categ) {
ul(
setting(
trans.letOtherPlayersFollowYou.frag(),
radios(form("follow"), booleanChoices)
),
setting(
trans.letOtherPlayersChallengeYou.frag(),
radios(form("challenge"), translatedChallengeChoices)
),
setting(
trans.letOtherPlayersMessageYou.frag(),
radios(form("message"), translatedMessageChoices)
),
setting(
trans.letOtherPlayersInviteYouToStudy.frag(),
radios(form("studyInvite"), translatedStudyInviteChoices)
),
setting(
trans.shareYourInsightsData.frag(),
radios(form("insightShare"), translatedInsightSquareChoices)
)
)
},
p(cls := "saved text none", dataIcon := "E")(trans.yourPreferencesHaveBeenSaved.frag())
val booleanChoices = Seq(0 -> trans.no.txt(), 1 -> trans.yes.txt())
div(cls := "account box box-pad")(
h1(bits.categName(categ)),
st.form(cls := "autosubmit", action := routes.Pref.formApply, method := "POST")(
categFieldset(PrefCateg.GameDisplay, categ)(
setting(
trans.pieceAnimation(),
radios(form("display.animation"), translatedAnimationChoices)
),
setting(
trans.materialDifference(),
radios(form("display.captured"), booleanChoices)
),
setting(
trans.boardHighlights(),
radios(form("display.highlight"), booleanChoices)
),
setting(
trans.pieceDestinations(),
radios(form("display.destination"), booleanChoices)
),
setting(
trans.boardCoordinates(),
radios(form("display.coords"), translatedBoardCoordinateChoices)
),
setting(
trans.moveListWhilePlaying(),
radios(form("display.replay"), translatedMoveListWhilePlayingChoices)
),
setting(
trans.pgnPieceNotation(),
radios(form("display.pieceNotation"), translatedPieceNotationChoices)
),
setting(
trans.zenMode(),
radios(form("display.zen"), booleanChoices)
),
setting(
"Display board resize handle",
radios(form("display.resizeHandle"), translatedBoardResizeHandleChoices)
),
setting(
trans.blindfoldChess(),
radios(form("display.blindfold"), translatedBlindfoldChoices)
)
)
),
categFieldset(PrefCateg.ChessClock, categ)(
setting(
trans.tenthsOfSeconds(),
radios(form("clockTenths"), translatedClockTenthsChoices)
),
setting(
trans.horizontalGreenProgressBars(),
radios(form("clockBar"), booleanChoices)
),
setting(
trans.soundWhenTimeGetsCritical(),
radios(form("clockSound"), booleanChoices)
)
),
categFieldset(PrefCateg.GameBehavior, categ)(
setting(
trans.howDoYouMovePieces(),
radios(form("behavior.moveEvent"), translatedMoveEventChoices)
),
setting(
trans.premovesPlayingDuringOpponentTurn(),
radios(form("behavior.premove"), booleanChoices)
),
setting(
trans.takebacksWithOpponentApproval(),
radios(form("behavior.takeback"), translatedTakebackChoices)
),
setting(
trans.promoteToQueenAutomatically(),
radios(form("behavior.autoQueen"), translatedAutoQueenChoices)
),
setting(
trans.claimDrawOnThreefoldRepetitionAutomatically(),
radios(form("behavior.autoThreefold"), translatedAutoThreefoldChoices)
),
setting(
trans.moveConfirmation(),
radios(form("behavior.submitMove"), submitMoveChoices)
),
setting(
trans.confirmResignationAndDrawOffers(),
radios(form("behavior.confirmResign"), confirmResignChoices)
),
setting(
trans.castleByMovingTheKingTwoSquaresOrOntoTheRook(),
radios(form("behavior.rookCastle"), translatedRookCastleChoices)
),
setting(
trans.inputMovesWithTheKeyboard(),
radios(form("behavior.keyboardMove"), booleanChoices)
)
),
categFieldset(PrefCateg.Privacy, categ)(
setting(
trans.letOtherPlayersFollowYou(),
radios(form("follow"), booleanChoices)
),
setting(
trans.letOtherPlayersChallengeYou(),
radios(form("challenge"), translatedChallengeChoices)
),
setting(
trans.letOtherPlayersMessageYou(),
radios(form("message"), translatedMessageChoices)
),
setting(
trans.letOtherPlayersInviteYouToStudy(),
radios(form("studyInvite"), translatedStudyInviteChoices)
),
setting(
trans.shareYourInsightsData(),
radios(form("insightShare"), translatedInsightSquareChoices)
)
),
p(cls := "saved text none", dataIcon := "E")(trans.yourPreferencesHaveBeenSaved())
)
}
)
}
}

View File

@ -17,38 +17,37 @@ object profile {
def apply(u: lila.user.User, form: play.api.data.Form[_])(implicit ctx: Context) = account.layout(
title = s"${u.username} - ${trans.editProfile.txt()}",
active = "editProfile",
evenMoreCss = cssTag("form3.css")
active = "editProfile"
) {
div(cls := "content_box small_box")(
h1(cls := "lichess_title text", dataIcon := "*")(trans.editProfile()),
st.form(cls := "form3", action := routes.Account.profileApply, method := "POST")(
div(cls := "form-group")(trans.allInformationIsPublicAndOptional()),
form3.split(
form3.group(form("country"), trans.country.frag(), half = true) { f =>
form3.select(f, lila.user.Countries.allPairs, default = "".some)
},
form3.group(form("location"), trans.location.frag(), half = true)(form3.input(_))
),
NotForKids {
form3.group(form("bio"), trans.biography.frag(), help = trans.biographyDescription.frag().some) { f =>
form3.textarea(f)(rows := 5)
}
div(cls := "account box box-pad")(
h1(trans.editProfile()),
st.form(cls := "form3", action := routes.Account.profileApply, method := "POST")(
div(cls := "form-group")(trans.allInformationIsPublicAndOptional()),
form3.split(
form3.group(form("country"), trans.country(), half = true) { f =>
form3.select(f, lila.user.Countries.allPairs, default = "".some)
},
form3.split(
form3.group(form("firstName"), trans.firstName.frag(), half = true)(form3.input(_)),
form3.group(form("lastName"), trans.lastName.frag(), half = true)(form3.input(_))
),
form3.split(
List("fide", "uscf", "ecf").map { rn =>
form3.group(form(s"${rn}Rating"), trans.xRating.frag(rn.toUpperCase), help = trans.ifNoneLeaveEmpty.frag().some, klass = "form-third")(form3.input(_, typ = "number"))
}
),
form3.group(form("links"), raw("Social media links "), help = Some(linksHelp)) { f =>
form3.group(form("location"), trans.location(), half = true)(form3.input(_))
),
NotForKids {
form3.group(form("bio"), trans.biography(), help = trans.biographyDescription().some) { f =>
form3.textarea(f)(rows := 5)
},
form3.actionHtml(form3.submit(trans.apply.frag()))
)
}
},
form3.split(
form3.group(form("firstName"), trans.firstName(), half = true)(form3.input(_)),
form3.group(form("lastName"), trans.lastName(), half = true)(form3.input(_))
),
form3.split(
List("fide", "uscf", "ecf").map { rn =>
form3.group(form(s"${rn}Rating"), trans.xRating(rn.toUpperCase), help = trans.ifNoneLeaveEmpty().some, klass = "form-third")(form3.input(_, typ = "number"))
}
),
form3.group(form("links"), raw("Social media links "), help = Some(linksHelp)) { f =>
form3.textarea(f)(rows := 5)
},
form3.action(form3.submit(trans.apply()))
)
}
)
}
}

View File

@ -0,0 +1,60 @@
package views.html
package account
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import controllers.routes
object security {
def apply(u: lila.user.User, sessions: List[lila.security.LocatedSession], curSessionId: String)(implicit ctx: Context) =
account.layout(title = s"${u.username} - ${trans.security.txt()}", active = "security") {
div(cls := "account security box")(
h1(trans.security()),
div(cls := "box__pad")(
p(trans.thisIsAListOfDevicesThatHaveLoggedIntoYourAccount()),
sessions.length > 1 option div(
trans.alternativelyYouCanX {
form(cls := "revoke-all", action := routes.Account.signout("all"), method := "POST")(
button(tpe := "submit", cls := "button button-empty button-red confirm")(
trans.revokeAllSessions()
)
)
}
)
),
table(cls := "slist slist-pad")(
sessions.map { s =>
tr(
td(cls := "icon")(
span(
cls := s"is-${if (s.session.id == curSessionId) "gold" else "green"}",
dataIcon := (if (s.session.isMobile) "" else "")
)
),
td(cls := "info")(
span(cls := "ip")(s.session.ip),
" ",
span(cls := "location")(s.location.map(_.toString)),
p(cls := "ua")(s.session.ua),
s.session.date.map { date =>
p(cls := "date")(
momentFromNow(date),
s.session.id == curSessionId option span(cls := "current")("[CURRENT]")
)
}
),
td(
s.session.id != curSessionId option
form(action := routes.Account.signout(s.session.id), method := "POST")(
button(tpe := "submit", cls := "button button-red", title := trans.logOut.txt(), dataIcon := "L")
)
)
)
}
)
)
}
}

View File

@ -1,50 +0,0 @@
@(u: User, sessions: List[lila.security.LocatedSession], curSessionId: String)(implicit ctx: Context)
@title = @{ s"${u.username} - ${trans.security.txt()}" }
@account.layout(title = title, active = "security") {
<div class="content_box no_padding high security">
<div class="signup_box">
<h1 class="lichess_title">@trans.security()</h1>
<p class="explanation">@trans.thisIsAListOfDevicesThatHaveLoggedIntoYourAccount()</p>
@if(sessions.length > 1) {
<div class="explanation">
@trans.alternativelyYouCanX {
<form class="revoke-all" action="@routes.Account.signout("all")" method="POST">
<button type="submit" class="button hint--top thin confirm">@trans.revokeAllSessions()</button>
</form>
}
</div>
}
<table class="slist">
@sessions.map { s =>
<tr>
<td class="icon">
<span class="is-@if(s.session.id == curSessionId){gold}else{green}" data-icon="@if(s.session.isMobile){}else{}"></span>
</td>
<td class="info">
<span class="ip">@s.session.ip</span>
<span class="location">@s.location</span>
<p class="ua">@s.session.ua</p>
@s.session.date.map { date =>
<p class="date">
@momentFromNow(date)
@if(s.session.id == curSessionId) { <span class="current">[CURRENT]</span> }
</p>
}
</td>
<td>
@if(s.session.id != curSessionId) {
<form action="@routes.Account.signout(s.session.id)" method="POST">
<button type="submit" class="button text hint--top" data-hint="@trans.logOut()">
<span data-icon="L"></span>
</button>
</form>
}
</td>
</tr>
}
</table>
</div>
</div>
}.toHtml

View File

@ -9,19 +9,18 @@ import controllers.routes
object twoFactor {
private val qrCode = raw("""<div style="width: 276px; height: 275px; padding: 10px; background: white; margin: 2em auto;"><div id="qrcode" style="width: 256px; height: 256px;"></div></div>""")
private val qrCode = raw("""<div style="width: 276px; height: 276px; padding: 10px; background: white; margin: 2em auto;"><div id="qrcode" style="width: 256px; height: 256px;"></div></div>""")
def setup(u: lila.user.User, form: play.api.data.Form[_])(implicit ctx: Context) = account.layout(
title = s"${u.username} - Two-factor authentication",
active = "twofactor",
evenMoreCss = cssTag("form3.css"),
evenMoreJs = frag(
jsAt("javascripts/vendor/qrcode.min.js"),
jsTag("twofactor.form.js")
)
) {
div(cls := "content_box small_box high twofactor")(
h1(cls := "lichess_title")("Setup two-factor authentication"),
div(cls := "account twofactor box box-pad")(
h1("Setup two-factor authentication"),
st.form(cls := "form3", action := routes.Account.setupTwoFactor, method := "POST")(
div(cls := "form-group")("Two-factor authentication adds another layer of security to your account."),
div(cls := "form-group")(
@ -31,35 +30,34 @@ object twoFactor {
qrCode,
div(cls := "form-group explanation")("Enter your password and the authentication code generated by the app to complete the setup. You will need an authentication code every time you log in."),
form3.hidden(form("secret")),
form3.password(form("passwd"), trans.password.frag()),
form3.group(form("token"), raw("Authentication code"))(form3.input(_)(pattern := "[0-9]{6}", autocomplete := "off", required := "")),
form3.password(form("passwd"), trans.password()),
form3.group(form("token"), raw("Authentication code"))(form3.input(_)(pattern := "[0-9]{6}", autocomplete := "off", required)),
form3.globalError(form),
div(cls := "form-group")("Note: If you lose access to your two-factor authentication codes, you can do a password reset via email."),
form3.actionHtml(form3.submit(raw("Enable two-factor authentication")))
form3.action(form3.submit(raw("Enable two-factor authentication")))
)
)
}
def disable(u: lila.user.User, form: play.api.data.Form[_])(implicit ctx: Context) = account.layout(
title = s"${u.username} - Two-factor authentication",
active = "twofactor",
evenMoreCss = cssTag("form3.css")
active = "twofactor"
) {
div(cls := "content_box small_box high twofactor")(
h1(cls := "lichess_title")(
raw("""<i data-icon="E" class="is-green"></i> """),
"Two-factor authentication enabled"
div(cls := "account twofactor box box-pad")(
h1(
i(cls := "is-green", dataIcon := "E"),
" Two-factor authentication enabled"
),
p("Your account is protected with two-factor authentication."),
st.form(cls := "form3", action := routes.Account.disableTwoFactor, method := "POST")(
p(
"You need your password and an authentication code from your authenticator app to disable two-factor authentication. ",
"If you lost access to your authentication codes, you can also do a password reset via email."
),
p(cls := "explanation")("Your account is protected with two-factor authentication."),
st.form(cls := "form3", action := routes.Account.disableTwoFactor, method := "POST")(
p(cls := "explanation")(
"You need your password and an authentication code from your authenticator app to disable two-factor authentication. ",
"If you lost access to your authentication codes, you can also do a password reset via email."
),
form3.password(form("passwd"), trans.password.frag()),
form3.group(form("token"), raw("Authentication code"))(form3.input(_)(pattern := "[0-9]{6}", autocomplete := "off", required := "")),
form3.actionHtml(form3.submit(raw("Disable two-factor authentication"), icon = None))
)
form3.password(form("passwd"), trans.password()),
form3.group(form("token"), raw("Authentication code"))(form3.input(_)(pattern := "[0-9]{6}", autocomplete := "off", required)),
form3.action(form3.submit(raw("Disable two-factor authentication"), icon = None))
)
}
)
}
}

View File

@ -11,16 +11,15 @@ object username {
def apply(u: lila.user.User, form: play.api.data.Form[_])(implicit ctx: Context) = account.layout(
title = s"${u.username} - ${trans.editProfile.txt()}",
active = "editUsername",
evenMoreCss = cssTag("form3.css")
active = "username"
) {
div(cls := "content_box small_box")(
h1(cls := "lichess_title text", dataIcon := "*")(trans.editProfile()),
st.form(cls := "form3", action := routes.Account.usernameApply, method := "POST")(
form3.globalError(form),
form3.group(form("userName"), trans.username.frag(), half = true, help = trans.changeUsernameDescription.frag().some)(form3.input(_)),
form3.actionHtml(form3.submit(trans.apply.frag()))
)
div(cls := "account box box-pad")(
h1(cls := "text", dataIcon := "*")(trans.changeUsername()),
st.form(cls := "form3", action := routes.Account.usernameApply, method := "POST")(
form3.globalError(form),
form3.group(form("username"), trans.username(), help = trans.changeUsernameDescription().some)(form3.input(_)(required)),
form3.action(form3.submit(trans.apply()))
)
}
)
}
}

View File

@ -1,14 +1,10 @@
package views.html
import play.twirl.api.Html
import scalatags.Text.tags2.section
import lila.activity.activities._
import lila.activity.model._
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.escapeHtml
import lila.user.User
import controllers.routes
@ -18,7 +14,7 @@ object activity {
def apply(u: User, as: Iterable[lila.activity.ActivityView])(implicit ctx: Context) =
div(cls := "activity")(
as.toSeq map { a =>
section(
st.section(
h2(semanticDate(a.interval.getStart)),
div(cls := "entries")(
a.patron map renderPatron,
@ -49,7 +45,9 @@ object activity {
private def renderPatron(p: Patron)(implicit ctx: Context) =
div(cls := "entry plan")(
iconTag(""),
div(trans.activity.supportedNbMonths.plural(p.months, p.months, Html(s"""<a href="${routes.Plan.index}">Patron</a>""")))
div(
trans.activity.supportedNbMonths.plural(p.months, p.months, a(href := routes.Plan.index)("Patron"))
)
)
private def renderPractice(p: Map[lila.practice.PracticeStudy, Int])(implicit ctx: Context) = {
@ -70,7 +68,7 @@ object activity {
case (study, nb) =>
val href = routes.Practice.show("-", study.slug, study.id.value)
frag(
trans.activity.practicedNbPositions.plural(nb, nb, Html(s"""<a href="$href">${study.name}</a>""")),
trans.activity.practicedNbPositions.plural(nb, nb, a(st.href := href)(study.name)),
br
)
}
@ -105,9 +103,8 @@ object activity {
posts.toSeq.map {
case (topic, posts) =>
val url = routes.ForumTopic.show(topic.categId, topic.slug)
val content = escapeHtml(shorten(topic.name, 70))
frag(
trans.activity.postedNbMessages.plural(posts.size, posts.size, Html(s"""<a href="$url">$content</a>""")),
trans.activity.postedNbMessages.plural(posts.size, posts.size, a(href := url)(shorten(topic.name, 70))),
subTag(
posts.map { post =>
div(cls := "line")(a(href := routes.ForumPost.redirect(post.id))(shorten(post.text, 120)))
@ -173,7 +170,7 @@ object activity {
if (in) trans.activity.gainedNbFollowers.pluralSame(f.actualNb)
else trans.activity.followedNbPlayers.pluralSame(f.actualNb),
subTag(
htmlList(f.ids.map(id => userIdLink(id.some))),
fragList(f.ids.map(id => userIdLink(id.some))),
f.nb.map { nb =>
frag(" and ", nb - maxSubEntries, " more")
}
@ -185,7 +182,7 @@ object activity {
private def renderSimuls(u: User)(simuls: List[lila.simul.Simul])(implicit ctx: Context) =
entryTag(
iconTag("|"),
iconTag("f"),
div(
simuls.groupBy(_.isHost(u.some)).toSeq.map {
case (isHost, simuls) => frag(
@ -226,7 +223,7 @@ object activity {
iconTag("f"),
div(
trans.activity.joinedNbTeams.pluralSame(teams.value.size),
subTag(htmlList(teams.value.map(id => teamLink(id))))
subTag(fragList(teams.value.map(id => teamLink(id))))
)
)
@ -245,7 +242,7 @@ object activity {
),
dataIcon := (t.rank <= 3).option("g")
)(
trans.activity.rankedInTournament.plural(t.nbGames, Html(s"""<strong>${t.rank}</strong>"""), (t.rankRatio.value * 100).toInt atLeast 1, t.nbGames, link),
trans.activity.rankedInTournament.plural(t.nbGames, strong(t.rank), (t.rankRatio.value * 100).toInt atLeast 1, t.nbGames, link),
br
)
}
@ -273,10 +270,10 @@ object activity {
s"""<score>${scoreStr("win", s.win, trans.nbWins)}${scoreStr("draw", s.draw, trans.nbDraws)}${scoreStr("loss", s.loss, trans.nbLosses)}</score>"""
}
private def ratingProgFrag(r: RatingProg)(implicit ctx: Context) = raw {
val prog = showProgress(r.diff, withTitle = false)
s"""<rating>${r.after.value}$prog</rating>"""
}
private def ratingProgFrag(r: RatingProg)(implicit ctx: Context) = ratingTag(
r.after.value,
ratingProgress(r.diff)
)
private def scoreStr(tag: String, p: Int, name: lila.i18n.I18nKey)(implicit ctx: Context) =
if (p == 0) ""

View File

@ -1,11 +1,9 @@
package views.html.analyse
import play.twirl.api.Html
import lila.analyse.Advice.Judgement
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.analyse.Advice.Judgement
object bits {
@ -20,18 +18,12 @@ object bits {
def layout(
title: String,
side: Option[Frag] = None,
chat: Option[Frag] = None,
underchat: Option[Frag] = None,
moreCss: Html = emptyHtml,
moreJs: Html = emptyHtml,
moreCss: Frag = emptyFrag,
moreJs: Frag = emptyFrag,
openGraph: Option[lila.app.ui.OpenGraph] = None
)(body: Html)(implicit ctx: Context): Frag =
)(body: Frag)(implicit ctx: Context): Frag =
views.html.base.layout(
title = title,
side = side.map(_.toHtml),
chat = chat,
underchat = underchat,
moreCss = moreCss,
moreJs = moreJs,
openGraph = openGraph,

View File

@ -1,44 +1,36 @@
package views.html.analyse
import play.api.libs.json.Json
import scalatags.Text.tags2.{ title => titleTag }
import play.api.libs.json.{ Json, JsObject }
import play.api.mvc.RequestHeader
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.EmbedConfig
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.safeJsonValue
import views.html.base.layout.{ bits => layout }
import controllers.routes
object embed {
import views.html.base.layout.bits._
import EmbedConfig.implicits._
private def bodyClass(implicit ctx: Context) = List(
"base" -> true,
ctx.currentTheme.cssClass -> true,
(if (ctx.currentBg == "transp") "dark transp" else ctx.currentBg) -> true
)
def apply(pov: lila.game.Pov, data: play.api.libs.json.JsObject)(implicit ctx: Context) = frag(
doctype,
htmlTag(ctx)(
topComment,
def apply(pov: lila.game.Pov, data: JsObject)(implicit config: EmbedConfig) = frag(
layout.doctype,
layout.htmlTag(config.lang)(
head(
charset,
metaCsp(none),
titleTag(s"${playerText(pov.game.whitePlayer)} vs ${playerText(pov.game.blackPlayer)} in ${pov.gameId} : ${pov.game.opening.fold(trans.analysis.txt())(_.opening.ecoName)}"),
fontStylesheets,
currentBgCss,
cssTags("common.css", "board.css", "analyse.css", "analyse-embed.css"),
pieceSprite
layout.charset,
layout.viewport,
layout.metaCsp(basicCsp withNonce config.nonce),
st.headTitle(replay titleOf pov),
layout.pieceSprite(lila.pref.PieceSet.default),
cssTagWithTheme("analyse.embed", config.bg)
),
body(cls := bodyClass ::: List(
"highlight" -> true,
"piece_letter" -> ctx.pref.pieceNotationIsLetter
body(cls := List(
s"highlight ${config.bg} ${config.board}" -> true
))(
div(cls := "is2d")(
div(cls := "embedded_analyse analyse cg-512")(miniBoardContent)
main(cls := "analyse")
),
footer {
val url = routes.Round.watcher(pov.gameId, pov.color.name)
@ -55,32 +47,31 @@ object embed {
jsTag("vendor/mousetrap.js"),
jsAt("compiled/util.js"),
jsAt("compiled/trans.js"),
jsAt("compiled/embed-analyse.js"),
analyseTag,
jsTag("embed-analyse.js"),
embedJs(s"""lichess.startEmbeddedAnalyse({
element: document.querySelector('.embedded_analyse'),
data: ${safeJsonValue(data)},
embed: true,
i18n: ${views.html.board.userAnalysisI18n(withCeval = false, withExplorer = false)}
});""")
embedJsUnsafe(s"""lichess.startEmbeddedAnalyse(${
safeJsonValue(Json.obj(
"data" -> data,
"embed" -> true,
"i18n" -> views.html.board.userAnalysisI18n(withCeval = false, withExplorer = false)
))
})""", config.nonce)
)
)
)
def notFound()(implicit ctx: Context) = frag(
doctype,
htmlTag(ctx)(
topComment,
def notFound(implicit config: EmbedConfig) = frag(
layout.doctype,
layout.htmlTag(config.lang)(
head(
charset,
metaCsp(none),
titleTag("404 - Game not found"),
fontStylesheets,
currentBgCss,
cssTags("common.css", "analyse-embed.css")
layout.charset,
layout.viewport,
layout.metaCsp(basicCsp),
st.headTitle("404 - Game not found"),
cssTagWithTheme("analyse.round.embed", "dark")
),
body(cls := bodyClass)(
div(cls := "not_found")(
body(cls := "dark")(
div(cls := "not-found")(
h1("Game not found")
)
)

View File

@ -19,7 +19,7 @@ object help {
private def k(str: String) = raw(s"""<kbd>$str</kbd>""")
def apply(isStudy: Boolean)(implicit ctx: Context) = frag(
h2(trans.keyboardShortcuts.frag()),
h2(trans.keyboardShortcuts()),
table(
tbody(
header("Navigate the move tree"),
@ -37,7 +37,7 @@ object help {
row(frag(k("x")), "Show threat"),
row(frag(k("e")), "Opening/endgame explorer"),
row(frag(k("f")), trans.flipBoard.txt()),
row(frag(k("/")), "Focus chat"),
row(frag(k("c")), "Focus chat"),
row(frag(k("shift"), k("C")), trans.keyShowOrHideComments.txt()),
row(frag(k("?")), "Show this help dialog"),
isStudy option frag(
@ -49,8 +49,8 @@ object help {
tr(
td(cls := "mouse", colspan := 2)(
ul(
li(trans.youCanAlsoScrollOverTheBoardToMoveInTheGame.frag()),
li(trans.analysisShapesHowTo.frag())
li(trans.youCanAlsoScrollOverTheBoardToMoveInTheGame()),
li(trans.analysisShapesHowTo())
)
)
)

View File

@ -2,12 +2,11 @@ package views.html.analyse
import lila.api.Context
import lila.app.templating.Environment._
import lila.common.String.html.safeJsonValue
import lila.i18n.{ I18nKeys => trans }
private object jsI18n {
def apply()(implicit ctx: Context) = safeJsonValue(i18nJsObject(translations))
def apply()(implicit ctx: Context) = i18nJsObject(translations)
private val translations = List(
trans.flipBoard,
@ -51,7 +50,6 @@ private object jsI18n {
trans.toggleLocalEvaluation,
// action menu
trans.menu,
trans.preferences,
trans.inlineNotation,
trans.computerAnalysis,
trans.enable,

View File

@ -1,11 +1,14 @@
package views.html.analyse
import play.twirl.api.Html
import play.api.libs.json.Json
import chess.variant.Crazyhouse
import bits.dataPanel
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.Lang
import lila.common.String.html.safeJsonValue
import lila.game.Pov
@ -13,6 +16,9 @@ import controllers.routes
object replay {
private[analyse] def titleOf(pov: Pov)(implicit lang: Lang) =
s"${playerText(pov.game.whitePlayer)} vs ${playerText(pov.game.blackPlayer)}: ${pov.game.opening.fold(trans.analysis.txt())(_.opening.ecoName)}"
def apply(
pov: Pov,
data: play.api.libs.json.JsObject,
@ -33,82 +39,101 @@ object replay {
views.html.chat.json(c.chat, name = trans.spectatorRoom.txt(), timeout = c.timeout, withNote = ctx.isAuth, public = true)
}
val pgnLinks = div(
a(dataIcon := "x", cls := "text", rel := "nofollow", href := s"${routes.Game.exportOne(game.id)}?literate=1")(trans.downloadAnnotated()),
a(dataIcon := "x", cls := "text", rel := "nofollow", href := s"${routes.Game.exportOne(game.id)}?evals=0&clocks=0")(trans.downloadRaw()),
game.isPgnImport option a(dataIcon := "x", cls := "text", rel := "nofollow", href := s"${routes.Game.exportOne(game.id)}?imported=1")(trans.downloadImported()),
ctx.noBlind option a(dataIcon := "=", cls := "text embed_howto", target := "_blank")(trans.embedInYourWebsite())
a(dataIcon := "x", cls := "text", href := s"${routes.Game.exportOne(game.id)}?literate=1")(trans.downloadAnnotated()),
a(dataIcon := "x", cls := "text", href := s"${routes.Game.exportOne(game.id)}?evals=0&clocks=0")(trans.downloadRaw()),
game.isPgnImport option a(dataIcon := "x", cls := "text", href := s"${routes.Game.exportOne(game.id)}?imported=1")(trans.downloadImported()),
ctx.noBlind option a(dataIcon := "=", cls := "text embed-howto", target := "_blank")(trans.embedInYourWebsite())
)
bits.layout(
title = s"${playerText(pov.game.whitePlayer)} vs ${playerText(pov.game.blackPlayer)}: ${game.opening.fold(trans.analysis.txt())(_.opening.ecoName)}",
side = views.html.game.side(pov, initialFen, none, simul = simul, userTv = userTv, bookmarked = bookmarked),
chat = views.html.chat.frag.some,
underchat = Some(views.html.round.bits underchat pov.game),
moreCss = cssTags("analyse.css", "chat.css"),
title = titleOf(pov),
moreCss = frag(
cssTag("analyse.round"),
pov.game.variant == Crazyhouse option cssTag("analyse.zh"),
ctx.blind option cssTag("round.nvui")
),
moreJs = frag(
analyseTag,
analyseNvuiTag,
embedJs(s"""lichess=lichess||{};
lichess.analyse={data:${safeJsonValue(data)},i18n:${jsI18n()},userId:$jsUserId,chat:${jsOrNull(chatJson)},
explorer:{endpoint:"$explorerEndpoint",tablebaseEndpoint:"$tablebaseEndpoint"}}""")
embedJsUnsafe(s"""lichess=lichess||{};lichess.analyse=${
safeJsonValue(Json.obj(
"data" -> data,
"i18n" -> jsI18n(),
"userId" -> ctx.userId,
"chat" -> chatJson,
"explorer" -> Json.obj(
"endpoint" -> explorerEndpoint,
"tablebaseEndpoint" -> tablebaseEndpoint
)
))
}""")
),
openGraph = povOpenGraph(pov).some
)(frag(
div(cls := "analyse cg-512")(
views.html.board.bits.domPreload(none)
main(cls := "analyse")(
st.aside(cls := "analyse__side")(
views.html.game.side(pov, initialFen, none, simul = simul, userTv = userTv, bookmarked = bookmarked)
),
chatOption.map(_ => views.html.chat.frag),
div(cls := "analyse__board main-board")(chessgroundSvg),
div(cls := "analyse__tools")(div(cls := "ceval")),
div(cls := "analyse__controls"),
!ctx.blind option frag(
div(cls := "analyse__underboard")(
div(cls := "analyse__underboard__panels")(
div(cls := "active"),
game.analysable option div(cls := "computer-analysis")(
if (analysis.isDefined || analysisStarted) div(id := "adv-chart")
else form(
cls := s"future-game-analysis${ctx.isAnon ?? " must-login"}",
action := routes.Analyse.requestAnalysis(gameId),
method := "post"
)(
button(`type` := "submit", cls := "button text")(
span(cls := "is3 text", dataIcon := "")(trans.requestAComputerAnalysis())
)
)
),
div(cls := "fen-pgn")(
div(
strong("FEN"),
input(readonly, spellcheck := false, cls := "copyable autoselect analyse__underboard__fen")
),
div(cls := "pgn-options")(
strong("PGN"),
pgnLinks
),
div(cls := "pgn")(pgn)
),
div(cls := "move-times")(
game.turns > 1 option div(id := "movetimes-chart")
),
cross.map { c =>
div(cls := "ctable")(
views.html.game.crosstable(pov.player.userId.fold(c)(c.fromPov), pov.gameId.some)
)
}
),
div(cls := "analyse__underboard__menu")(
game.analysable option
span(
cls := "computer-analysis",
dataPanel := "computer-analysis",
title := analysis.map { a => s"Provided by ${usernameOrId(a.providedBy)}" }
)(trans.computerAnalysis()),
!game.isPgnImport option frag(
game.turns > 1 option span(dataPanel := "move-times")(trans.moveTimes()),
cross.isDefined option span(dataPanel := "ctable")(trans.crosstable())
),
span(dataPanel := "fen-pgn")(raw("FEN &amp; PGN"))
)
)
)
),
if (ctx.blind) div(cls := "blind_content none")(
if (ctx.blind) div(cls := "blind-content none")(
h2("PGN downloads"),
pgnLinks
)
else div(cls := "underboard_content none")(
div(cls := "analysis_panels")(
game.analysable option div(cls := "panel computer_analysis")(
if (analysis.isDefined || analysisStarted) div(id := "adv_chart")
else form(
cls := s"future_game_analysis${ctx.isAnon ?? " must_login"}",
action := routes.Analyse.requestAnalysis(gameId),
method := "post"
)(
button(`type` := "submit", cls := "button text")(
span(cls := "is3 text", dataIcon := "")(trans.requestAComputerAnalysis())
)
)
),
div(cls := "panel fen_pgn")(
div(
strong("FEN"),
input(readonly := true, spellcheck := false, cls := "copyable autoselect fen")
),
div(cls := "pgn_options")(
strong("PGN"),
pgnLinks
),
div(cls := "pgn")(pgn)
),
div(cls := "panel move_times")(
game.turns > 1 option div(id := "movetimes_chart")
),
cross.map { c =>
div(cls := "panel crosstable")(
views.html.game.crosstable(pov.player.userId.fold(c)(c.fromPov), pov.gameId.some)
)
}
),
div(cls := "analysis_menu")(
game.analysable option
a(
dataPanel := "computer_analysis",
cls := "computer_analysis",
title := analysis.map { a => s"Provided by ${usernameOrId(a.providedBy)}" }
)(trans.computerAnalysis()),
!game.isPgnImport option frag(
game.turns > 1 option a(dataPanel := "move_times", cls := "move_times")(trans.moveTimes()),
cross.isDefined option a(dataPanel := "crosstable", cls := "crosstable")(trans.crosstable())
),
a(dataPanel := "fen_pgn", cls := "fen_pgn")(raw("FEN &amp; PGN"))
)
)
))
}
}

View File

@ -1,7 +1,5 @@
package views.html.analyse
import play.twirl.api.Html
import bits.dataPanel
import lila.api.Context
import lila.app.templating.Environment._
@ -22,65 +20,31 @@ object replayBot {
import pov._
views.html.analyse.bits.layout(
title = s"${playerText(pov.player)} vs ${playerText(pov.opponent)} in $gameId : ${game.opening.fold(trans.analysis.txt())(_.opening.ecoName)}",
side = views.html.game.side(pov, initialFen, none, simul = simul, bookmarked = false),
chat = none,
underchat = Some(views.html.game.bits.watchers),
moreCss = cssTag("analyse.css"),
title = replay titleOf pov,
moreCss = cssTag("analyse.round"),
openGraph = povOpenGraph(pov).some
) {
frag(
div(cls := "analyse cg-512")(
views.html.board.bits.domPreload(pov.some),
div(cls := "lichess_ground for_bot")(
h1(titleGame(pov.game)),
p(describePov(pov)),
p(pov.game.opening.map(_.opening.ecoName))
)
main(cls := "analyse")(
st.aside(cls := "analyse__side")(
views.html.game.side(pov, initialFen, none, simul = simul, bookmarked = false)
),
analysis.map { a =>
div(cls := "advice_summary")(
table(
a.summary map {
case (color, pairs) => frag(
thead(
tr(
td(span(cls := s"is color-icon $color"))
),
th(playerLink(pov.game.player(color), withOnline = false))
),
tbody(
pairs map {
case (judgment, nb) => tr(
td(strong(nb)),
th(bits.judgmentName(judgment))
)
},
tr(
td(strong(lila.analyse.Accuracy.mean(pov.withColor(color), a))),
th(trans.averageCentipawnLoss())
),
tr(td(cls := "spacerlol", colspan := 2))
)
)
}
)
)
},
div(cls := "underboard_content")(
div(cls := "analysis_panels")(
div(cls := "panel fen_pgn")(
div(cls := "analyse__board main-board")(chessgroundSvg),
div(cls := "analyse__tools")(div(cls := "ceval")),
div(cls := "analyse__controls"),
div(cls := "analyse__underboard")(
div(cls := "analyse__underboard__panels")(
div(cls := "fen-pgn active")(
div(
strong("FEN"),
input(readonly, spellcheck := false, cls := "copyable autoselect analyse__underboard__fen")
),
div(cls := "pgn")(pgn)
),
cross.map { c =>
div(cls := "panel crosstable")(
div(cls := "ctable active")(
views.html.game.crosstable(pov.player.userId.fold(c)(c.fromPov), pov.gameId.some)
)
}
),
div(cls := "analysis_menu")(
cross.isDefined option a(dataPanel := "crosstable", cls := "crosstable")(trans.crosstable()),
a(dataPanel := "fen_pgn", cls := "fen_pgn")(raw("FEN &amp; PGN"))
)
)
)

View File

@ -0,0 +1,131 @@
package views.html
package auth
import play.api.data.{ Form, Field }
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.user.User
import controllers.routes
object bits {
def formFields(username: Field, password: Field, emailOption: Option[Field], register: Boolean)(implicit ctx: Context) = frag(
form3.group(username, if (register) trans.username() else trans.usernameOrEmail()) { f =>
frag(
form3.input(f)(autofocus, required),
p(cls := "error exists none")(trans.usernameAlreadyUsed())
)
},
form3.password(password, trans.password()),
emailOption.map { email =>
form3.group(email, trans.email(), help = frag("We will only use it for password reset.").some)(form3.input(_, typ = "email")(required))
}
)
def passwordReset(form: Form[_], captcha: lila.common.Captcha, ok: Option[Boolean] = None)(implicit ctx: Context) =
views.html.base.layout(
title = trans.passwordReset.txt(),
moreCss = cssTag("auth"),
moreJs = captchaTag
) {
main(cls := "auth auth-signup box box-pad")(
h1(
ok.map { r =>
span(cls := (if (r) "is-green" else "is-red"), dataIcon := (if (r) "E" else "L"))
},
trans.passwordReset()
),
st.form(
cls := "form3",
action := routes.Auth.passwordResetApply,
method := "post"
)(
form3.group(form("email"), trans.email())(form3.input(_, typ = "email")(autofocus)),
views.html.base.captcha(form, captcha),
form3.action(form3.submit(trans.emailMeALink()))
)
)
}
def passwordResetSent(email: String)(implicit ctx: Context) =
views.html.base.layout(
title = trans.passwordReset.txt()
) {
main(cls := "page-small box box-pad")(
h1(cls := "is-green text", dataIcon := "E")(trans.checkYourEmail()),
p(trans.weHaveSentYouAnEmailTo(email)),
p(trans.ifYouDoNotSeeTheEmailCheckOtherPlaces())
)
}
def passwordResetConfirm(u: User, token: String, form: Form[_], ok: Option[Boolean] = None)(implicit ctx: Context) =
views.html.base.layout(
title = s"${u.username} - ${trans.changePassword.txt()}",
moreCss = cssTag("form3")
) {
main(cls := "page-small box box-pad")(
(ok match {
case Some(true) => h1(cls := "is-green text", dataIcon := "E")
case Some(false) => h1(cls := "is-red text", dataIcon := "L")
case _ => h1
})(
userLink(u, withOnline = false),
" - ",
trans.changePassword()
),
st.form(cls := "form3", action := routes.Auth.passwordResetConfirmApply(token), method := "POST")(
form3.hidden(form("token")),
form3.passwordModified(form("newPasswd1"), trans.newPassword())(autofocus),
form3.password(form("newPasswd2"), trans.newPasswordAgain()),
form3.globalError(form),
form3.action(form3.submit(trans.changePassword()))
)
)
}
def checkYourEmailBanner(userEmail: lila.security.EmailConfirm.UserEmail) = frag(
styleTag("""
body { margin-top: 45px; }
#email-confirm {
height: 40px;
background: #3893E8;
color: #fff!important;
font-size: 1.3em;
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
border-bottom: 1px solid #666;
box-shadow: 0 5px 6px rgba(0, 0, 0, 0.3);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 107;
}
#email-confirm a {
color: #fff!important;
text-decoration: underline;
margin-left: 1em;
}
"""),
div(id := "email-confirm")(
s"Almost there, ${userEmail.username}! Now check your email (${userEmail.email.conceal}) for signup confirmation.",
a(href := routes.Auth.checkYourEmail)("Click here for help")
)
)
def tor()(implicit ctx: Context) =
views.html.base.layout(
title = "Tor exit node"
) {
main(cls := "page-small box box-pad")(
h1(cls := "text", dataIcon := "2")("Ooops"),
p("Sorry, you can't signup to lichess through TOR!"),
p("As an Anonymous user, you can play, train, and use all lichess features.")
)
}
}

View File

@ -0,0 +1,68 @@
package views.html.auth
import play.api.data.Form
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import controllers.routes
object checkYourEmail {
def apply(
userEmail: Option[lila.security.EmailConfirm.UserEmail],
form: Option[Form[_]] = None
)(implicit ctx: Context) =
views.html.base.layout(
title = "Check your email",
moreCss = cssTag("email-confirm")
) {
main(cls := s"page-small box box-pad email-confirm ${if (form.exists(_.hasErrors)) "error" else "anim"}")(
h1(cls := "is-green text", dataIcon := "E")(trans.checkYourEmail()),
p(trans.weHaveSentYouAnEmailClickTheLink()),
h2("Not receiving it?"),
ol(
li(h3(trans.ifYouDoNotSeeTheEmailCheckOtherPlaces())),
userEmail.map(_.email).map { email =>
li(
h3("Make sure your email address is correct:"),
br, br,
st.form(action := routes.Auth.fixEmail, method := "POST")(
input(
id := "new-email",
tpe := "email",
required,
name := "email",
value := form.flatMap(_("email").value).getOrElse(email.value),
pattern := s"^((?!^${email.value}$$).)*$$"
),
embedJsUnsafe("""
var email = document.getElementById("new-email");
var currentError = "This is already your current email.";
email.setCustomValidity(currentError);
email.addEventListener("input", function() {
email.setCustomValidity(email.validity.patternMismatch ? currentError : "");
});"""),
button(tpe := "submit", cls := "button")("Change it"),
form.map { f =>
errMsg(f("email"))
}
)
)
},
li(
h3("Wait up to 5 minutes."), br,
"Depending on your email provider, it can take a while to arrive."
),
li(
h3("Still not getting it?"), br,
"Did you make sure your email address is correct?", br,
"Did you wait 5 minutes?", br,
"If so, ",
a(href := routes.Account.emailConfirmHelp)("proceed to this page to solve the issue"), "."
)
)
)
}
}

View File

@ -1,54 +0,0 @@
@(userEmail: Option[lila.security.EmailConfirm.UserEmail], form: Option[Form[_]] = None)(implicit ctx: Context)
@auth.layout(title = "Check your email") {
<div class="content_box small_box signup email_confirm @if(form.exists(_.hasErrors)){error}else{anim}">
<div class="signup_box">
<h1 class="lichess_title is-green text" data-icon="E">@trans.checkYourEmail()</h1>
<p>@trans.weHaveSentYouAnEmailClickTheLink()</p>
<h2>Not receiving it?</h2>
<ol>
<li>
<h3>@trans.ifYouDoNotSeeTheEmailCheckOtherPlaces()</h3>
</li>
@userEmail.map(_.email).map { email =>
<li>
<h3>Make sure your email address is correct:</h3><br />
<form action="@routes.Auth.fixEmail" method="POST">
<input
id="new_email"
type="email"
required="required"
name="email"
value="@form.flatMap(_("email").value).getOrElse(email.value)"
pattern="^((?!^@{email.value}$).)*$" />
<script>
var email = document.getElementById("new_email");
var currentError = "This is already your current email.";
email.setCustomValidity(currentError);
email.addEventListener("input", function() {
email.setCustomValidity(email.validity.patternMismatch ? currentError : "");
});
</script>
<button type="submit">Change it</button>
@form.map { f =>
@errMsg(f("email"))
}
</form>
</li>
}
<li>
<h3>Wait up to 10 minutes.</h3><br />
Depending on your email provider, it can take a while to arrive.
</li>
<li>
<h3>Still not getting it?</h3><br />
Did you make sure your email address is correct?<br />
Did you wait 10 minutes?<br />
If so, <a class="blue", href="@routes.Account.emailConfirmHelp">proceed to this page to solve the issue</a>.
</li>
</ol>
<br />
<br />
</div>
</div>
}

View File

@ -1,35 +0,0 @@
@(userEmail: lila.security.EmailConfirm.UserEmail)
<style type="text/css">
body {
margin-top: 45px;
}
#email_confirm {
height: 40px;
background: #3893E8;
color: #fff!important;
font-size: 1.3em;
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
border-bottom: 1px solid #666;
box-shadow: 0 5px 6px rgba(0, 0, 0, 0.3);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
#email_confirm a {
color: #fff!important;
text-decoration: underline;
margin-left: 1em;
}
</style>
<div id="email_confirm">
Almost there, @userEmail.username! Now check your email (@userEmail.email.conceal) for signup confirmation.
<a href="@routes.Auth.checkYourEmail">Click here for help</a>
</div>

View File

@ -1,11 +0,0 @@
@(username: Field, password: Field, emailOption: Option[Field], register: Boolean)(implicit ctx: Context)
@import lila.app.ui.ScalatagsTwirlForm._
@form3.group(username, if(register) trans.username.frag() else trans.usernameOrEmail.frag()) { f =>
@form3.inputHtml(f)(*.autofocus := true)
<p class="error exists none">@trans.usernameAlreadyUsed()</p>
}
@form3.group(password, trans.password.frag())(form3.input(_, typ = "password"))
@emailOption.map { email =>
@form3.group(email, trans.email.frag())(form3.input(_, typ = "email"))
}

View File

@ -1,6 +0,0 @@
@(title: String, moreJs: Html = emptyHtml, formCss: Boolean = false, csp: Option[lila.common.ContentSecurityPolicy] = None)(body: Html)(implicit ctx: Context)
@base.layout(
title = title,
moreCss = cssTags(List("user-signup.css" -> true, "form3.css" -> formCss)),
moreJs = moreJs,
csp = csp)(body).toHtml

View File

@ -0,0 +1,47 @@
package views.html
package auth
import play.api.data.Form
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import controllers.routes
object login {
val twoFactorHelp = span(dataIcon := "")(
"Open the two-factor authentication app on your device to view your authentication code and verify your identity."
)
def apply(form: Form[_], referrer: Option[String])(implicit ctx: Context) = views.html.base.layout(
title = trans.signIn.txt(),
moreJs = jsTag("login.js"),
moreCss = cssTag("auth")
) {
main(cls := "auth auth-login box box-pad")(
h1(trans.signIn()),
st.form(
cls := "form3",
action := s"${routes.Auth.authenticate}${referrer.?? { ref => s"?referrer=${java.net.URLEncoder.encode(ref, "US-ASCII")}" }}",
method := "post"
)(
div(cls := "one-factor")(
form3.globalError(form),
auth.bits.formFields(form("username"), form("password"), none, register = false),
form3.submit(trans.signIn(), icon = none)
),
div(cls := "two-factor none")(
form3.group(form("token"), raw("Authentication code"), help = Some(twoFactorHelp))(form3.input(_)(autocomplete := "off", pattern := "[0-9]{6}")),
p(cls := "error none")("Invalid code."),
form3.submit(trans.signIn(), icon = none)
)
),
div(cls := "alternative")(
a(href := routes.Auth.signup())(trans.signUp()),
a(href := routes.Auth.passwordReset())(trans.passwordReset())
)
)
}
}

View File

@ -1,42 +0,0 @@
@(form: Form[_], referrer: Option[String])(implicit ctx: Context)
@import lila.app.ui.ScalatagsTwirlForm._
@twoFactorHelp = {
<span data-icon="">Open the two-factor authentication app on your device to view your authentication code and verify your identity.</span>
}
@auth.layout(
title = trans.signIn.txt(),
moreJs = jsTag("login.js"),
formCss = true) {
<div class="content_box login">
<div class="signup_box">
<h1 class="lichess_title">@trans.signIn()</h1>
<form class="form3 login" action="@routes.Auth.authenticate@referrer.map { ref =>?referrer=@{java.net.URLEncoder.encode(ref, "US-ASCII")}}" method="POST">
<div class="one-factor">
@form3.globalError(form)
@auth.formFields(form("username"), form("password"), none, register = false)
@form3.actionHtml(form3.submit(trans.signIn.frag(), icon = "F".some))
</div>
<div class="two-factor none">
@form3.group(form("token"), raw("Authentication code"), help = Some(twoFactorHelp))(form3.input(_)(*.autocomplete := "off", *.pattern := "[0-9]{6}"))
<p class="error none">Invalid code.</p>
@form3.actionHtml(form3.submit(trans.signIn.frag(), icon = "F".some))
</form>
</div>
<div class="alternative">
@trans.newToLichess()
<br />
<br />
<a href="@routes.Auth.signup()" class="button" data-icon="F"> @trans.signUp()</a>
<br />
<br />
<br />
@trans.forgotPassword()
<br />
<br />
<a href="@routes.Auth.passwordReset()" class="button" data-icon="F"> @trans.passwordReset()</a>
</div>
</div>
</div>
}

View File

@ -1,22 +0,0 @@
@(form: Form[_], captcha: lila.common.Captcha, ok: Option[Boolean] = None)(implicit ctx: Context)
@auth.layout(
title = trans.passwordReset.txt(),
formCss= true) {
<div class="content_box small_box signup">
<div class="signup_box">
<h1 class="lichess_title text">
@ok.map {
case true => {<span class="is-green" data-icon="E"></span>}
case false => {<span class="is-red" data-icon="L"></span>}
}
@trans.passwordReset()
</h1>
<form class="form3" action="@routes.Auth.passwordResetApply" method="POST">
@form3.group(form("email"), trans.email.frag())(form3.input(_, typ = "email"))
@base.captcha(form, captcha).toHtml
@form3.actionHtml(form3.submit(trans.emailMeALink.frag(), icon = "F".some))
</form>
</div>
</div>
}

View File

@ -1,24 +0,0 @@
@(u: User, token: String, form: Form[_], ok: Option[Boolean] = None)(implicit ctx: Context)
@title = @{ s"${u.username} - ${trans.changePassword.txt()}" }
@auth.layout(title = title, formCss = true) {
<div class="content_box small_box">
<div class="signup_box">
<h1 class="lichess_title text">
@userLink(u, withOnline = false) - @trans.changePassword()
@ok.map {
case true => {<span class="is-green" data-icon="E"></span>}
case false => {<span class="is-red" data-icon="L"></span>}
}
</h1>
<form class="form3" action="@routes.Auth.passwordResetConfirmApply(token)" method="POST">
@form3.hidden(form("token"))
@form3.password(form("newPasswd1"), trans.newPassword.frag()).toHtml
@form3.password(form("newPasswd2"), trans.newPasswordAgain.frag()).toHtml
@form3.globalError(form)
@form3.actionHtml(form3.submit(trans.changePassword.frag()))
</form>
</div>
</div>
}

View File

@ -1,12 +0,0 @@
@(email: String)(implicit ctx: Context)
@auth.layout(
title = trans.passwordReset.txt()) {
<div class="content_box small_box signup">
<div class="signup_box">
<h1 class="lichess_title is-green text" data-icon="E">@trans.checkYourEmail()</h1>
<p>@trans.weHaveSentYouAnEmailTo(email)</p>
<p>@trans.ifYouDoNotSeeTheEmailCheckOtherPlaces()</p>
</div>
</div>
}

View File

@ -0,0 +1,51 @@
package views.html
package auth
import play.api.data.Form
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import controllers.routes
object signup {
private val recaptchaScript = raw("""<script src="https://www.google.com/recaptcha/api.js" async defer></script>""")
def apply(form: Form[_], recaptcha: lila.security.RecaptchaPublicConfig)(implicit ctx: Context) =
views.html.base.layout(
title = trans.signUp.txt(),
moreJs = frag(
jsTag("signup.js"),
recaptcha.enabled option recaptchaScript,
fingerprintTag
),
moreCss = cssTag("auth"),
csp = defaultCsp.withRecaptcha.some
) {
main(cls := "auth auth-signup box box-pad")(
h1(trans.signUp()),
st.form(
id := "signup_form",
cls := "form3",
action := routes.Auth.signupPost,
method := "post"
)(
auth.bits.formFields(form("username"), form("password"), form("email").some, register = true),
input(id := "signup-fp-input", name := "fp", tpe := "hidden"),
div(cls := "form-group text", dataIcon := "")(
trans.computersAreNotAllowedToPlay(), br,
small(trans.byRegisteringYouAgreeToBeBoundByOur(a(href := routes.Page.tos)(trans.termsOfService())))
),
if (recaptcha.enabled)
button(
cls := "g-recaptcha submit button text big",
attr("data-sitekey") := recaptcha.key,
attr("data-callback") := "signupSubmit"
)(trans.signUp())
else form3.submit(trans.signUp(), icon = none, klass = "big")
)
)
}
}

View File

@ -1,44 +0,0 @@
@(form: Form[_], recaptcha: lila.security.RecaptchaPublicConfig)(implicit ctx: Context)
@moreJs = {
@jsTag("signup.js")
@if(recaptcha.enabled) {
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
}
@fingerprintTag
}
@tosLink = {
<a href="@routes.Page.tos">@trans.termsOfService()</a>
}
@auth.layout(
title = trans.signUp.txt(),
moreJs = moreJs,
formCss = true,
csp = defaultCsp.withRecaptcha.some) {
<div class="content_box small_box signup">
<div class="alternative">
@trans.haveAnAccount()
<a href="@routes.Auth.login()" class="text button" data-icon="F">@trans.signIn()</a>
</div>
<div class="signup_box">
<h1 class="lichess_title">@trans.signUp()</h1>
<form id="signup_form" class="form3" action="@routes.Auth.signupPost" method="POST">
@auth.formFields(form("username"), form("password"), form("email").some, register = true)
<input id="signup-fp-input" name="fp" type="hidden" />
<div class="form-group text" data-icon="">
@trans.computersAreNotAllowedToPlay()<br />
<small>@trans.byRegisteringYouAgreeToBeBoundByOur(tosLink)</small>
</div>
@form3.actionHtml {
@if(recaptcha.enabled) {
<button class="g-recaptcha submit button text big" data-icon="F" data-sitekey="@recaptcha.key" data-callback='signupSubmit'>@trans.signUp()</button>
} else {
@form3.submit(trans.signUp.frag(), icon = "F".some, klass = "big")
}
}
</form>
</div>
</div>
}

View File

@ -1,16 +0,0 @@
@()(implicit ctx: Context)
@auth.layout(
title = "Tor exit node") {
<div class="content_box small_box signup">
<div class="signup_box">
<h1 class="lichess_title text" data-icon="2">Ooops</h1>
<p>
Sorry, you can't signup to lichess through TOR!
<br />
<br />
As an Anonymous user, you can play, train, and use all lichess features.
</p>
</div>
</div>
}

View File

@ -1,14 +0,0 @@
@()(implicit ctx: Context)
@base.layout(
title = "Access denied",
moreCss = cssTag("authFailed.css")) {
<div class="content_box small_box">
<header>
<h1>403</h1>
<strong>Access denied!</strong>
<p>You tried to visit a page you're not authorized to access. Return to <a class="underline" href="@routes.Lobby.home">the homepage</a>.<p>
</header>
</div>
}.toHtml

View File

@ -0,0 +1,31 @@
package views.html.base
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
object bits {
def mselect(id: String, current: Frag, items: List[Frag]) = div(cls := "mselect")(
input(tpe := "checkbox", cls := "mselect__toggle fullscreen-toggle", st.id := s"mselect-$id", aria.label := "Other variants"),
label(`for` := s"mselect-$id", cls := "mselect__label")(current),
label(`for` := s"mselect-$id", cls := "fullscreen-mask"),
st.nav(cls := "mselect__list")(items)
)
lazy val stage = a(
href := "https://lichess.org",
style := """
background: #7f1010;
color: #fff;
position: fixed;
bottom: 0;
left: 0;
padding: .5em 1em;
border-top-right-radius: 3px;
z-index: 99;
"""
)(
"This is an empty lichess preview website, go to lichess.org instead"
)
}

View File

@ -29,27 +29,29 @@ object captcha {
),
dataCheckUrl := routes.Main.captchaCheck(captcha.gameId)
)(
div(
cls := "mini_board parse_fen is2d",
dataPlayable := "1",
dataX := encodeFen(safeJsonValue(Json.toJson(captcha.moves))),
dataY := encodeFen(if (captcha.white) { "white" } else { "black" }),
dataZ := encodeFen(captcha.fen)
)(miniBoardContent),
div(cls := "challenge")(
div(
cls := "mini-board cg-board-wrap parse-fen is2d",
dataPlayable := "1",
dataX := encodeFen(safeJsonValue(Json.toJson(captcha.moves))),
dataY := encodeFen(if (captcha.white) { "white" } else { "black" }),
dataZ := encodeFen(captcha.fen)
)(div(cls := "cg-board"))
),
div(cls := "captcha-explanation")(
label(cls := "form-label")(trans.colorPlaysCheckmateInOne.frag(
(if (captcha.white) trans.white else trans.black).frag()
label(cls := "form-label")(trans.colorPlaysCheckmateInOne(
(if (captcha.white) trans.white else trans.black)()
)),
br, br,
trans.thisIsAChessCaptcha.frag(),
trans.thisIsAChessCaptcha(),
br,
trans.clickOnTheBoardToMakeYourMove.frag(),
trans.clickOnTheBoardToMakeYourMove(),
br, br,
trans.help.frag(),
trans.help(),
" ",
a(cls := "hint--bottom", dataHint := trans.viewTheSolution.txt(), target := "_blank", href := url)(url),
div(cls := "result success text", dataIcon := "E")(trans.checkmate.frag()),
div(cls := "result failure text", dataIcon := "k")(trans.notACheckmate.frag()),
a(title := trans.viewTheSolution.txt(), target := "_blank", href := url)(url),
div(cls := "result success text", dataIcon := "E")(trans.checkmate()),
div(cls := "result failure text", dataIcon := "k")(trans.notACheckmate()),
form3.hidden(form("move"))
)
)

View File

@ -0,0 +1,25 @@
package views.html
package base
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import controllers.routes
object errorPage {
def apply(ex: Throwable)(implicit ctx: Context) = layout(
title = "Internal server error"
) {
main(cls := "page-small box box-pad")(
h1("Something went wrong on this page"),
p(
"If the problem persists, please ",
a(href := s"${routes.Main.contact}#help-error-page")("report the bug"),
"."
),
code(ex.getMessage)
)
}
}

View File

@ -1,14 +0,0 @@
@(ex: Throwable)(implicit ctx: Context)
@base.layout(title = "Internal server error") {
<div class="content_box small_box">
<h1>Something went wrong on this page.</h1>
<br />
<br />
<p>If the problem persists, please <a href="@routes.Main.contact#error-page">report the bug</a>.</p>
<br />
<br />
<code>@ex.getMessage</code>
</div>
}.toHtml

View File

@ -1,10 +0,0 @@
@(field: play.api.data.Field, options: Iterable[(Any,String)], default: Option[String] = None)
<select name="@field.name">
@default.map { d =>
<option value="">@d</option>
}
@options.map { v =>
<option value="@v._1" @(if(field.value == Some(v._1.toString)) "selected" else "")>@v._2</option>
}
</select>

View File

@ -1,95 +0,0 @@
@()(implicit ctx: Context)
<div class="inner">
<div class="body">
@ctx.me.map { me =>
<div class="user">
<section><h2>@me.username</h2></section>
<div class="perfs">
@topBarSortedPerfTypes.map { pt =>
@me.perfs(pt.key).map { perf =>
<a href="@routes.User.perfStat(me.username, pt.key)" class="perf@if(perf.nb == 0){ nope}" data-icon="@pt.iconChar">
<h3>@pt.name</h3>
@if(perf.nb > 0) {
<div class="rating">
<strong>@perf.glicko.intRating</strong>
@showProgress(perf.progress, withTitle = false)
</div>
} else { N/A }
}
</a>
}
</div>
</div>
}.getOrElse {
<div class="anon">
<section>
<h2>@trans.signIn()</h2>
<a class="login" href="@routes.Auth.login">@trans.signIn()</a>
<div class="forgot">
<a href="@routes.Auth.passwordReset">@trans.forgotPassword()</a>
</div>
</section>
<section class="signup">
<h2>@trans.newToLichess()</h2>
<a class="signup" href="@routes.Auth.signup">@trans.signUp()</a>
</section>
</div>
}
<div class="menu">
<section>
<h2>@trans.play()</h2>
<a href="/?any#hook">@trans.createAGame()</a>
<a href="@routes.Tournament.home()">@trans.tournament()</a>
<a href="@routes.Simul.home">@trans.simultaneousExhibitions()</a>
<a href="@routes.Tv.index">Lichess TV</a>
<a href="@routes.Tv.games">@trans.currentGames()</a>
</section>
<section>
<h2>@trans.learnMenu()</h2>
<a href="@routes.Puzzle.home">@trans.training()</a>
<a href="@routes.Practice.index">Practice</a>
<a href="@routes.Coordinate.home">@trans.coordinates.coordinates()</a>
<a href="@routes.Study.allDefault(1)">Study</a>
<a href="@routes.Video.index">@trans.videoLibrary()</a>
</section>
<section>
<h2>@trans.community()</h2>
<a href="@routes.User.list">@trans.players()</a>
@NotForKids {
<a href="@routes.Team.home()">@trans.teams()</a>
<a href="@routes.ForumCateg.index">@trans.forum()</a>
}
</section>
<section>
<h2>@trans.tools()</h2>
<a href="@routes.Editor.index">@trans.boardEditor()</a>
<a href="@routes.UserAnalysis.index">@trans.analysis()</a>
<a href="@routes.Importer.importGame">@trans.importGame()</a>
<a href="@routes.Search.index()">@trans.advancedSearch()</a>
</section>
</div>
</div>
<div class="footer">
@NotForKids {
<a href="/mobile">@trans.mobileApp()</a> ı
}
<a href="/blog">@trans.blog()</a> ı
@NotForKids {
<a href="/developers">@trans.webmasters()</a> ı
<a href="/about">@trans.about()</a> ı
<a href="/help/contribute">@trans.contribute()</a> ı
}
<a href="/thanks">@trans.thankYou()</a><br />
@NotForKids {
<a href="/patron">@trans.donate()</a> ı
}
<a href="/contact">@trans.contact()</a> ı
<a href="@routes.Page.tos">@trans.termsOfService()</a> ı
<a href="@routes.Page.privacy">@trans.privacy()</a>
@NotForKids {
ı <a href="https://database.lichess.org/" target="_blank">@trans.database()</a>
ı <a href="https://github.com/ornicar/lila" target="_blank">@trans.sourceCode()</a>
}
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More