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)