From 45d670486c809bb72b9f0455117208480e98a92e Mon Sep 17 00:00:00 2001 From: Srihari Srinivasan Date: Mon, 28 Nov 2022 21:54:28 -0500 Subject: [PATCH 1/2] Add embed route for puzzles with zen mode enforced --- app/controllers/Puzzle.scala | 48 +++++++++++++- app/views/base/layout.scala | 13 +++- app/views/lobby/home.scala | 2 +- app/views/puzzle/dailyEmbed.scala | 35 +++++++++++ app/views/puzzle/embed.scala | 100 ++++++++++++++++++++++-------- conf/routes | 1 + 6 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 app/views/puzzle/dailyEmbed.scala diff --git a/app/controllers/Puzzle.scala b/app/controllers/Puzzle.scala index 90f4585589d8f..9b3938b369adc 100644 --- a/app/controllers/Puzzle.scala +++ b/app/controllers/Puzzle.scala @@ -61,6 +61,27 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): ).enableSharedArrayBuffer } + private def renderEmbed( + puzzle: Puz, + angle: PuzzleAngle, + color: Option[Color] = None, + replay: Option[lila.puzzle.PuzzleReplay] = None, + langPath: Option[LangPath] = None + )(implicit ctx: Context) = + renderJson(puzzle, angle, replay) zip + ctx.me.??(u => env.puzzle.session.getSettings(u) dmap some) map { case (json, settings) => + Ok( + views.html.puzzle + .embed( + puzzle, + json, + env.puzzle.jsonView.pref(ctx.pref), + settings | PuzzleSettings.default(color), + langPath + ) + ).enableSharedArrayBuffer + } + def daily = Open { implicit ctx => NoBot { @@ -346,6 +367,29 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): } } + private def serveEmbed(angleOrId: String)(implicit ctx: Context) = + NoBot { + val langPath = LangPath(routes.Puzzle.show(angleOrId)).some + PuzzleAngle find angleOrId match { + case Some(angle) => + nextPuzzleForMe(angle, none) flatMap { + renderEmbed(_, angle, langPath = langPath) + } + case _ if angleOrId.size == Puz.idSize => + OptionFuResult(env.puzzle.api.puzzle find Puz.Id(angleOrId)) { puzzle => + ctx.me.?? { env.puzzle.api.casual.setCasualIfNotYetPlayed(_, puzzle) } >> + renderEmbed(puzzle, PuzzleAngle.mix, langPath = langPath) + } + case _ => + angleOrId.toLongOption + .flatMap(Puz.numericalId.apply) + .??(env.puzzle.api.puzzle.find) map { + case None => Redirect(routes.Puzzle.home) + case Some(puz) => Redirect(routes.Puzzle.show(puz.id.value)) + } + } + } + def showWithAngle(angleKey: String, id: PuzzleId) = Open { implicit ctx => NoBot { val angle = PuzzleAngle.findOrMix(angleKey) @@ -374,10 +418,12 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): Action.async { implicit req => env.puzzle.daily.get map { case None => NotFound - case Some(daily) => html.puzzle.embed(daily) + case Some(daily) => html.puzzle.dailyEmbed(daily) } } + def embed(angleOrId: String) = Open(serveEmbed(angleOrId)(_)) + def activity = Scoped(_.Puzzle.Read) { req => me => val config = lila.puzzle.PuzzleActivity.Config( diff --git a/app/views/base/layout.scala b/app/views/base/layout.scala index 0aa9b4453a35a..cae123d629990 100644 --- a/app/views/base/layout.scala +++ b/app/views/base/layout.scala @@ -113,6 +113,14 @@ object layout: ${trans.preferences.zenMode.txt()} """) + private def getZenZone(zen: Boolean)(implicit ctx: Context): Option[scalatags.Text.RawFrag] = + { + if (zen) + return None + else + return Some(zenZone) + } + private def dasher(me: lila.user.User) = div(cls := "dasher")( a(id := "user_tag", cls := "toggle link", href := routes.Auth.logoutGet)(me.username), @@ -232,6 +240,7 @@ object layout: chessground: Boolean = true, zoomable: Boolean = false, zenable: Boolean = false, + zen: Boolean = false, csp: Option[ContentSecurityPolicy] = None, wrapClass: String = "", atomLinkTag: Option[Tag] = None, @@ -291,7 +300,7 @@ object layout: baseClass -> true, "dark-board" -> (ctx.pref.bg == lila.pref.Pref.Bg.DARKBOARD), "piece-letter" -> ctx.pref.pieceNotationIsLetter, - "zen" -> ctx.pref.isZen, + "zen" -> (if (zen) zen else ctx.pref.isZen), "blind-mode" -> ctx.blind, "kid" -> ctx.kid, "mobile" -> ctx.isMobileBrowser, @@ -322,7 +331,7 @@ object layout: .get(ctx.req) .ifTrue(ctx.isAnon) .map(views.html.auth.bits.checkYourEmailBanner(_)), - zenable option zenZone, + zenable option getZenZone(zen), siteHeader.apply, div( id := "main-wrap", diff --git a/app/views/lobby/home.scala b/app/views/lobby/home.scala index 511d1086f6e41..8ef8b1e762b95 100644 --- a/app/views/lobby/home.scala +++ b/app/views/lobby/home.scala @@ -122,7 +122,7 @@ object home: ) }, puzzle map { p => - views.html.puzzle.embed.dailyLink(p)(ctx.lang)(cls := "lobby__puzzle") + views.html.puzzle.dailyEmbed.dailyLink(p)(ctx.lang)(cls := "lobby__puzzle") }, ctx.noBot option bits.underboards(tours, simuls, leaderboard, tournamentWinners), bits.lastPosts(lastPost, ublogPosts), diff --git a/app/views/puzzle/dailyEmbed.scala b/app/views/puzzle/dailyEmbed.scala new file mode 100644 index 0000000000000..541c5359281e7 --- /dev/null +++ b/app/views/puzzle/dailyEmbed.scala @@ -0,0 +1,35 @@ +package views.html.puzzle + +import controllers.routes +import play.api.i18n.Lang + +import lila.app.templating.Environment.{ given, * } +import lila.app.ui.EmbedConfig +import lila.app.ui.ScalatagsTemplate.{ *, given } +import lila.puzzle.DailyPuzzle + +object embed: + + import EmbedConfig.implicits.* + + def apply(daily: DailyPuzzle.WithHtml)(implicit config: EmbedConfig) = + views.html.base.embed( + title = "lichess.org chess puzzle", + cssModule = "tv.embed" + )( + dailyLink(daily)(config.lang)( + targetBlank, + id := "daily-puzzle", + cls := "embedded" + ), + jsModule("puzzle.embed") + ) + + def dailyLink(daily: DailyPuzzle.WithHtml)(implicit lang: Lang) = a( + href := routes.Puzzle.daily, + title := trans.puzzle.clickToSolve.txt() + )( + span(cls := "text")(trans.puzzle.puzzleOfTheDay()), + raw(daily.html), + span(cls := "text")(daily.puzzle.color.fold(trans.whitePlays, trans.blackPlays)()) + ) diff --git a/app/views/puzzle/embed.scala b/app/views/puzzle/embed.scala index 541c5359281e7..7508e0861b470 100644 --- a/app/views/puzzle/embed.scala +++ b/app/views/puzzle/embed.scala @@ -2,34 +2,84 @@ package views.html.puzzle import controllers.routes import play.api.i18n.Lang +import play.api.libs.json.{ JsObject, Json } -import lila.app.templating.Environment.{ given, * } +import lila.api.Context +import lila.app.templating.Environment._ import lila.app.ui.EmbedConfig -import lila.app.ui.ScalatagsTemplate.{ *, given } -import lila.puzzle.DailyPuzzle +import lila.app.ui.ScalatagsTemplate._ +import lila.common.Json.colorWrites +import lila.common.String.html.safeJsonValue -object embed: +object embed { - import EmbedConfig.implicits.* + import EmbedConfig.implicits._ - def apply(daily: DailyPuzzle.WithHtml)(implicit config: EmbedConfig) = - views.html.base.embed( - title = "lichess.org chess puzzle", - cssModule = "tv.embed" - )( - dailyLink(daily)(config.lang)( - targetBlank, - id := "daily-puzzle", - cls := "embedded" + def apply( + puzzle: lila.puzzle.Puzzle, + data: JsObject, + pref: JsObject, + settings: lila.puzzle.PuzzleSettings, + langPath: Option[lila.common.LangPath] = None + )(implicit ctx: Context) = { + val isStreak = data.value.contains("streak") + views.html.base.layout( + title = if (isStreak) "Puzzle Streak" else trans.puzzles.txt(), + zen = true, + moreCss = frag( + cssTag("puzzle"), + ctx.pref.hasKeyboardMove option cssTag("keyboardMove"), + ctx.blind option cssTag("round.nvui") ), - jsModule("puzzle.embed") - ) - - def dailyLink(daily: DailyPuzzle.WithHtml)(implicit lang: Lang) = a( - href := routes.Puzzle.daily, - title := trans.puzzle.clickToSolve.txt() - )( - span(cls := "text")(trans.puzzle.puzzleOfTheDay()), - raw(daily.html), - span(cls := "text")(daily.puzzle.color.fold(trans.whitePlays, trans.blackPlays)()) - ) + moreJs = frag( + puzzleTag, + puzzleNvuiTag, + embedJsUnsafeLoadThen(s"""LichessPuzzle(${safeJsonValue( + Json + .obj( + "data" -> data, + "pref" -> pref, + "i18n" -> bits.jsI18n(streak = isStreak), + "showRatings" -> ctx.pref.showRatings, + "settings" -> Json.obj("difficulty" -> settings.difficulty.key).add("color" -> settings.color) + ) + .add("themes" -> ctx.isAuth.option(bits.jsonThemes)) + )})""") + ), + csp = analysisCsp.some, + chessground = false, + openGraph = lila.app.ui + .OpenGraph( + image = cdnUrl( + routes.Export.puzzleThumbnail(puzzle.id.value, ctx.pref.theme.some, ctx.pref.pieceSet.some).url + ).some, + title = + if (isStreak) "Puzzle Streak" + else s"Chess tactic #${puzzle.id} - ${puzzle.color.name.capitalize} to play", + url = s"$netBaseUrl${routes.Puzzle.show(puzzle.id.value).url}", + description = + if (isStreak) trans.puzzle.streakDescription.txt() + else + s"Lichess tactic trainer: ${puzzle.color + .fold( + trans.puzzle.findTheBestMoveForWhite, + trans.puzzle.findTheBestMoveForBlack + ) + .txt()}. Played by ${puzzle.plays} players." + ) + .some, + zoomable = true, + zenable = true, + withHrefLangs = langPath + ) { + main(cls := "puzzle")( + st.aside(cls := "puzzle__side")( + div(cls := "puzzle__side__metas") + ), + div(cls := "puzzle__board main-board")(chessgroundBoard), + div(cls := "puzzle__tools"), + div(cls := "puzzle__controls") + ) + } + } +} diff --git a/conf/routes b/conf/routes index 502488071a523..b53bbd39fa482 100644 --- a/conf/routes +++ b/conf/routes @@ -152,6 +152,7 @@ GET /training/new controllers.Puzzle.mobileBcNew GET /training/$numericalId<\d{6,}>/load controllers.Puzzle.mobileBcLoad(numericalId: Long) POST /training/$numericalId<\d{6,}>/vote controllers.Puzzle.mobileBcVote(numericalId: Long) GET /training/:angleOrId controllers.Puzzle.show(angleOrId) +GET /training/embed/:angleOrId controllers.Puzzle.embed(angleOrId) GET /training/:angle/$color controllers.Puzzle.angleAndColor(angle, color) GET /training/:angle/$id<\w{5}> controllers.Puzzle.showWithAngle(angle, id) POST /training/$numericalId<\d{6,}>/round2 controllers.Puzzle.mobileBcRound(numericalId: Long) From bafdda4a018882272247b0dec282ceb160c30da2 Mon Sep 17 00:00:00 2001 From: Srihari Srinivasan Date: Mon, 28 Nov 2022 22:30:46 -0500 Subject: [PATCH 2/2] Add embed route for puzzles with zen mode enforced --- app/controllers/Puzzle.scala | 48 +++++++++++++- app/views/base/layout.scala | 13 +++- app/views/lobby/home.scala | 2 +- app/views/puzzle/dailyEmbed.scala | 35 +++++++++++ app/views/puzzle/embed.scala | 100 ++++++++++++++++++++++-------- conf/routes | 1 + 6 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 app/views/puzzle/dailyEmbed.scala diff --git a/app/controllers/Puzzle.scala b/app/controllers/Puzzle.scala index 90f4585589d8f..9b3938b369adc 100644 --- a/app/controllers/Puzzle.scala +++ b/app/controllers/Puzzle.scala @@ -61,6 +61,27 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): ).enableSharedArrayBuffer } + private def renderEmbed( + puzzle: Puz, + angle: PuzzleAngle, + color: Option[Color] = None, + replay: Option[lila.puzzle.PuzzleReplay] = None, + langPath: Option[LangPath] = None + )(implicit ctx: Context) = + renderJson(puzzle, angle, replay) zip + ctx.me.??(u => env.puzzle.session.getSettings(u) dmap some) map { case (json, settings) => + Ok( + views.html.puzzle + .embed( + puzzle, + json, + env.puzzle.jsonView.pref(ctx.pref), + settings | PuzzleSettings.default(color), + langPath + ) + ).enableSharedArrayBuffer + } + def daily = Open { implicit ctx => NoBot { @@ -346,6 +367,29 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): } } + private def serveEmbed(angleOrId: String)(implicit ctx: Context) = + NoBot { + val langPath = LangPath(routes.Puzzle.show(angleOrId)).some + PuzzleAngle find angleOrId match { + case Some(angle) => + nextPuzzleForMe(angle, none) flatMap { + renderEmbed(_, angle, langPath = langPath) + } + case _ if angleOrId.size == Puz.idSize => + OptionFuResult(env.puzzle.api.puzzle find Puz.Id(angleOrId)) { puzzle => + ctx.me.?? { env.puzzle.api.casual.setCasualIfNotYetPlayed(_, puzzle) } >> + renderEmbed(puzzle, PuzzleAngle.mix, langPath = langPath) + } + case _ => + angleOrId.toLongOption + .flatMap(Puz.numericalId.apply) + .??(env.puzzle.api.puzzle.find) map { + case None => Redirect(routes.Puzzle.home) + case Some(puz) => Redirect(routes.Puzzle.show(puz.id.value)) + } + } + } + def showWithAngle(angleKey: String, id: PuzzleId) = Open { implicit ctx => NoBot { val angle = PuzzleAngle.findOrMix(angleKey) @@ -374,10 +418,12 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): Action.async { implicit req => env.puzzle.daily.get map { case None => NotFound - case Some(daily) => html.puzzle.embed(daily) + case Some(daily) => html.puzzle.dailyEmbed(daily) } } + def embed(angleOrId: String) = Open(serveEmbed(angleOrId)(_)) + def activity = Scoped(_.Puzzle.Read) { req => me => val config = lila.puzzle.PuzzleActivity.Config( diff --git a/app/views/base/layout.scala b/app/views/base/layout.scala index 0aa9b4453a35a..cae123d629990 100644 --- a/app/views/base/layout.scala +++ b/app/views/base/layout.scala @@ -113,6 +113,14 @@ object layout: ${trans.preferences.zenMode.txt()} """) + private def getZenZone(zen: Boolean)(implicit ctx: Context): Option[scalatags.Text.RawFrag] = + { + if (zen) + return None + else + return Some(zenZone) + } + private def dasher(me: lila.user.User) = div(cls := "dasher")( a(id := "user_tag", cls := "toggle link", href := routes.Auth.logoutGet)(me.username), @@ -232,6 +240,7 @@ object layout: chessground: Boolean = true, zoomable: Boolean = false, zenable: Boolean = false, + zen: Boolean = false, csp: Option[ContentSecurityPolicy] = None, wrapClass: String = "", atomLinkTag: Option[Tag] = None, @@ -291,7 +300,7 @@ object layout: baseClass -> true, "dark-board" -> (ctx.pref.bg == lila.pref.Pref.Bg.DARKBOARD), "piece-letter" -> ctx.pref.pieceNotationIsLetter, - "zen" -> ctx.pref.isZen, + "zen" -> (if (zen) zen else ctx.pref.isZen), "blind-mode" -> ctx.blind, "kid" -> ctx.kid, "mobile" -> ctx.isMobileBrowser, @@ -322,7 +331,7 @@ object layout: .get(ctx.req) .ifTrue(ctx.isAnon) .map(views.html.auth.bits.checkYourEmailBanner(_)), - zenable option zenZone, + zenable option getZenZone(zen), siteHeader.apply, div( id := "main-wrap", diff --git a/app/views/lobby/home.scala b/app/views/lobby/home.scala index 511d1086f6e41..8ef8b1e762b95 100644 --- a/app/views/lobby/home.scala +++ b/app/views/lobby/home.scala @@ -122,7 +122,7 @@ object home: ) }, puzzle map { p => - views.html.puzzle.embed.dailyLink(p)(ctx.lang)(cls := "lobby__puzzle") + views.html.puzzle.dailyEmbed.dailyLink(p)(ctx.lang)(cls := "lobby__puzzle") }, ctx.noBot option bits.underboards(tours, simuls, leaderboard, tournamentWinners), bits.lastPosts(lastPost, ublogPosts), diff --git a/app/views/puzzle/dailyEmbed.scala b/app/views/puzzle/dailyEmbed.scala new file mode 100644 index 0000000000000..c64041f4b637d --- /dev/null +++ b/app/views/puzzle/dailyEmbed.scala @@ -0,0 +1,35 @@ +package views.html.puzzle + +import controllers.routes +import play.api.i18n.Lang + +import lila.app.templating.Environment.{ given, * } +import lila.app.ui.EmbedConfig +import lila.app.ui.ScalatagsTemplate.{ *, given } +import lila.puzzle.DailyPuzzle + +object dailyEmbed: + + import EmbedConfig.implicits.* + + def apply(daily: DailyPuzzle.WithHtml)(implicit config: EmbedConfig) = + views.html.base.embed( + title = "lichess.org chess puzzle", + cssModule = "tv.embed" + )( + dailyLink(daily)(config.lang)( + targetBlank, + id := "daily-puzzle", + cls := "embedded" + ), + jsModule("puzzle.embed") + ) + + def dailyLink(daily: DailyPuzzle.WithHtml)(implicit lang: Lang) = a( + href := routes.Puzzle.daily, + title := trans.puzzle.clickToSolve.txt() + )( + span(cls := "text")(trans.puzzle.puzzleOfTheDay()), + raw(daily.html), + span(cls := "text")(daily.puzzle.color.fold(trans.whitePlays, trans.blackPlays)()) + ) diff --git a/app/views/puzzle/embed.scala b/app/views/puzzle/embed.scala index 541c5359281e7..7508e0861b470 100644 --- a/app/views/puzzle/embed.scala +++ b/app/views/puzzle/embed.scala @@ -2,34 +2,84 @@ package views.html.puzzle import controllers.routes import play.api.i18n.Lang +import play.api.libs.json.{ JsObject, Json } -import lila.app.templating.Environment.{ given, * } +import lila.api.Context +import lila.app.templating.Environment._ import lila.app.ui.EmbedConfig -import lila.app.ui.ScalatagsTemplate.{ *, given } -import lila.puzzle.DailyPuzzle +import lila.app.ui.ScalatagsTemplate._ +import lila.common.Json.colorWrites +import lila.common.String.html.safeJsonValue -object embed: +object embed { - import EmbedConfig.implicits.* + import EmbedConfig.implicits._ - def apply(daily: DailyPuzzle.WithHtml)(implicit config: EmbedConfig) = - views.html.base.embed( - title = "lichess.org chess puzzle", - cssModule = "tv.embed" - )( - dailyLink(daily)(config.lang)( - targetBlank, - id := "daily-puzzle", - cls := "embedded" + def apply( + puzzle: lila.puzzle.Puzzle, + data: JsObject, + pref: JsObject, + settings: lila.puzzle.PuzzleSettings, + langPath: Option[lila.common.LangPath] = None + )(implicit ctx: Context) = { + val isStreak = data.value.contains("streak") + views.html.base.layout( + title = if (isStreak) "Puzzle Streak" else trans.puzzles.txt(), + zen = true, + moreCss = frag( + cssTag("puzzle"), + ctx.pref.hasKeyboardMove option cssTag("keyboardMove"), + ctx.blind option cssTag("round.nvui") ), - jsModule("puzzle.embed") - ) - - def dailyLink(daily: DailyPuzzle.WithHtml)(implicit lang: Lang) = a( - href := routes.Puzzle.daily, - title := trans.puzzle.clickToSolve.txt() - )( - span(cls := "text")(trans.puzzle.puzzleOfTheDay()), - raw(daily.html), - span(cls := "text")(daily.puzzle.color.fold(trans.whitePlays, trans.blackPlays)()) - ) + moreJs = frag( + puzzleTag, + puzzleNvuiTag, + embedJsUnsafeLoadThen(s"""LichessPuzzle(${safeJsonValue( + Json + .obj( + "data" -> data, + "pref" -> pref, + "i18n" -> bits.jsI18n(streak = isStreak), + "showRatings" -> ctx.pref.showRatings, + "settings" -> Json.obj("difficulty" -> settings.difficulty.key).add("color" -> settings.color) + ) + .add("themes" -> ctx.isAuth.option(bits.jsonThemes)) + )})""") + ), + csp = analysisCsp.some, + chessground = false, + openGraph = lila.app.ui + .OpenGraph( + image = cdnUrl( + routes.Export.puzzleThumbnail(puzzle.id.value, ctx.pref.theme.some, ctx.pref.pieceSet.some).url + ).some, + title = + if (isStreak) "Puzzle Streak" + else s"Chess tactic #${puzzle.id} - ${puzzle.color.name.capitalize} to play", + url = s"$netBaseUrl${routes.Puzzle.show(puzzle.id.value).url}", + description = + if (isStreak) trans.puzzle.streakDescription.txt() + else + s"Lichess tactic trainer: ${puzzle.color + .fold( + trans.puzzle.findTheBestMoveForWhite, + trans.puzzle.findTheBestMoveForBlack + ) + .txt()}. Played by ${puzzle.plays} players." + ) + .some, + zoomable = true, + zenable = true, + withHrefLangs = langPath + ) { + main(cls := "puzzle")( + st.aside(cls := "puzzle__side")( + div(cls := "puzzle__side__metas") + ), + div(cls := "puzzle__board main-board")(chessgroundBoard), + div(cls := "puzzle__tools"), + div(cls := "puzzle__controls") + ) + } + } +} diff --git a/conf/routes b/conf/routes index 502488071a523..b53bbd39fa482 100644 --- a/conf/routes +++ b/conf/routes @@ -152,6 +152,7 @@ GET /training/new controllers.Puzzle.mobileBcNew GET /training/$numericalId<\d{6,}>/load controllers.Puzzle.mobileBcLoad(numericalId: Long) POST /training/$numericalId<\d{6,}>/vote controllers.Puzzle.mobileBcVote(numericalId: Long) GET /training/:angleOrId controllers.Puzzle.show(angleOrId) +GET /training/embed/:angleOrId controllers.Puzzle.embed(angleOrId) GET /training/:angle/$color controllers.Puzzle.angleAndColor(angle, color) GET /training/:angle/$id<\w{5}> controllers.Puzzle.showWithAngle(angle, id) POST /training/$numericalId<\d{6,}>/round2 controllers.Puzzle.mobileBcRound(numericalId: Long)