Merge branch 'master' into l10n_master
commit
c480513aaa
|
@ -67,7 +67,6 @@ public/piece-src/spatial | [Maurizio Monge](http://poisson.phc.unipi.it/~monge/c
|
|||
public/piece-src/reillycraig | [Reilly Craig](https://instagram.com/fader_) |
|
||||
public/piece/other | [Twitch](http://lazythunk.com/kappa/) |
|
||||
public/images/staunton/staunton/Staunton | [Eden Murs](https://userstyles.org/styles/134558/lichess-pieces-3d-staunton) (?) |
|
||||
public/vendor/ChessPursuit | [Saturnyn](https://github.com/Saturnyn) | [CC BY-NC-SA](https://github.com/Saturnyn/ChessPursuit/issues/1#issuecomment-141067515)
|
||||
|
||||
* The other sounds in public/sound.
|
||||
* The other artwork in public/images.
|
||||
|
|
|
@ -94,11 +94,23 @@ object Auth extends LilaController {
|
|||
implicit val req = ctx.req
|
||||
req.session get "sessionId" foreach lila.security.Store.delete
|
||||
negotiate(
|
||||
html = fuccess(Redirect(routes.Main.mobile)),
|
||||
api = apiVersion => Ok(Json.obj("ok" -> true)).fuccess
|
||||
html = Redirect(routes.Main.mobile).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
|
||||
Ok(Json.obj("ok" -> true)).withCookies(LilaCookie.newSession).fuccess
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def signup = Open { implicit ctx =>
|
||||
NoTor {
|
||||
Ok(html.auth.signup(forms.signup.website, env.RecaptchaPublicKey)).fuccess
|
||||
|
|
|
@ -142,7 +142,7 @@ object Main extends LilaController {
|
|||
Ok(html.base.fpmenu()).fuccess
|
||||
}
|
||||
|
||||
def fishnet = Open { implicit ctx =>
|
||||
Ok(html.site.fishnet()).fuccess
|
||||
def getFishnet = Open { implicit ctx =>
|
||||
Ok(html.site.getFishnet()).fuccess
|
||||
}
|
||||
}
|
||||
|
|
|
@ -177,8 +177,8 @@ object Tournament extends LilaController {
|
|||
}
|
||||
|
||||
def terminate(id: String) = Secure(_.TerminateTournament) { implicit ctx => me =>
|
||||
OptionResult(repo startedById id) { tour =>
|
||||
env.api finish tour
|
||||
OptionResult(repo byId id) { tour =>
|
||||
env.api kill tour
|
||||
Env.mod.logApi.terminateTournament(me.id, tour.fullName)
|
||||
Redirect(routes.Tournament show tour.id)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import chess.{ Status => S, Color, Clock, Mode }
|
|||
import controllers.routes
|
||||
import play.twirl.api.Html
|
||||
|
||||
import lila.common.String.html.{ escape => escapeHtml }
|
||||
import lila.common.String.html.escapeHtmlUnsafe
|
||||
import lila.game.{ Game, Player, Namer, Pov }
|
||||
import lila.user.{ User, UserContext }
|
||||
import lila.i18n.{ I18nKeys, enLang }
|
||||
|
@ -126,7 +126,7 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
|
|||
val klass = cssClass.??(" " + _)
|
||||
val content = (player.aiLevel, player.name) match {
|
||||
case (Some(level), _) => aiNameHtml(level, withRating).body
|
||||
case (_, Some(name)) => escapeHtml(name)
|
||||
case (_, Some(name)) => escapeHtmlUnsafe(name)
|
||||
case _ => User.anonymous
|
||||
}
|
||||
s"""<span class="user_link$klass">$content$statusIcon</span>"""
|
||||
|
|
|
@ -14,91 +14,12 @@ trait StringHelper { self: NumberHelper =>
|
|||
|
||||
val slugify = lila.common.String.slugify _
|
||||
|
||||
val escapeHtml = lila.common.String.html.escape _
|
||||
|
||||
private val escapeHtmlUnsafe = lila.common.String.html.escapeUnsafe _
|
||||
|
||||
def shorten(text: String, length: Int, sep: String = "…"): Html = {
|
||||
val t = text.replace("\n", " ")
|
||||
if (t.size > (length + sep.size)) Html(escapeHtmlUnsafe(t take length) ++ sep)
|
||||
else escapeHtml(t)
|
||||
}
|
||||
|
||||
def shortenWithBr(text: String, length: Int) = Html {
|
||||
nl2brUnsafe(escapeHtmlUnsafe(text).take(length)).replace("<br /><br />", "<br />")
|
||||
}
|
||||
|
||||
def pluralize(s: String, n: Int) = s"$n $s${if (n > 1) "s" else ""}"
|
||||
|
||||
def autoLink(text: String): Html = nl2br(addUserProfileLinksUnsafe(addLinksUnsafe(escapeHtmlUnsafe(text))))
|
||||
|
||||
private def nl2brUnsafe(text: String): String =
|
||||
text.replace("\r\n", "<br />").replace("\n", "<br />")
|
||||
|
||||
def nl2br(text: String) = Html(nl2brUnsafe(text))
|
||||
|
||||
private val markdownLinkRegex = """\[([^\[]+)\]\(([^\)]+)\)""".r
|
||||
|
||||
def markdownLinks(text: String): Html = nl2br {
|
||||
markdownLinkRegex.replaceAllIn(escapeHtmlUnsafe(text), m => {
|
||||
s"""<a href="${m group 2}">${m group 1}</a>"""
|
||||
})
|
||||
}
|
||||
|
||||
def repositionTooltip(link: Html, position: String) = Html {
|
||||
link.body.replace("<a ", s"""<a data-pt-pos="$position" """)
|
||||
}
|
||||
|
||||
private val urlRegex = """(?i)\b((https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,6}\/)((?:[`!\[\]{};:'".,<>?«»“”‘’]*[^\s`!\[\]{}\(\);:'".,<>?«»“”‘’])*))""".r
|
||||
|
||||
/**
|
||||
* Creates hyperlinks to user profiles mentioned using the '@' prefix. e.g. @ornicar
|
||||
* @param text The text to regex match
|
||||
* @return The text as a HTML hyperlink
|
||||
*/
|
||||
def addUserProfileLinks(text: String) = Html(addUserProfileLinksUnsafe(text))
|
||||
|
||||
private def addUserProfileLinksUnsafe(text: String): String =
|
||||
User.atUsernameRegex.replaceAllIn(text, m => {
|
||||
val user = m group 1
|
||||
val url = s"//$netDomain/@/$user"
|
||||
|
||||
s"""<a href="$url">@$user</a>"""
|
||||
})
|
||||
|
||||
def addLinks(text: String) = Html(addLinksUnsafe(text))
|
||||
|
||||
private def addLinksUnsafe(text: String): String = try {
|
||||
urlRegex.replaceAllIn(text, m => {
|
||||
if (m.group(0) contains """) m.group(0)
|
||||
else if (m.group(2) == "http://" || m.group(2) == "https://") {
|
||||
if (s"${m.group(3)}/" startsWith s"$netDomain/") {
|
||||
// internal
|
||||
val link = m.group(3)
|
||||
s"""<a rel="nofollow" href="//$link">$link</a>"""
|
||||
} else {
|
||||
// external
|
||||
val link = m.group(1)
|
||||
s"""<a rel="nofollow" href="$link" target="_blank">$link</a>"""
|
||||
}
|
||||
} else {
|
||||
if (s"${m.group(2)}/" startsWith s"$netDomain/") {
|
||||
// internal
|
||||
val link = m.group(1)
|
||||
s"""<a rel="nofollow" href="//$link">$link</a>"""
|
||||
} else {
|
||||
// external
|
||||
val link = m.group(1)
|
||||
s"""<a rel="nofollow" href="http://$link" target="_blank">$link</a>"""
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
case e: IllegalArgumentException =>
|
||||
lila.log("templating").error(s"addLinks($text)", e)
|
||||
text
|
||||
}
|
||||
|
||||
def showNumber(n: Int): String = if (n > 0) s"+$n" else n.toString
|
||||
|
||||
implicit def lilaRichString(str: String) = new {
|
||||
|
|
|
@ -6,7 +6,7 @@ import play.twirl.api.Html
|
|||
|
||||
import lila.api.Context
|
||||
import lila.team.Env.{ current => teamEnv }
|
||||
import lila.common.String.html.{ escape => escapeHtml }
|
||||
import lila.common.String.html.escapeHtml
|
||||
|
||||
trait TeamHelper {
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package lila.app
|
||||
package ui
|
||||
|
||||
import lila.common.String.html.{ escape => escapeHtml }
|
||||
import lila.common.String.html.escapeHtmlUnsafe
|
||||
import play.twirl.api.Html
|
||||
|
||||
case class OpenGraph(
|
||||
|
@ -19,7 +19,7 @@ case class OpenGraph(
|
|||
object og {
|
||||
|
||||
private def tag(name: String, value: String) =
|
||||
s"""<meta property="og:$name" content="${escapeHtml(value)}"/>"""
|
||||
s"""<meta property="og:$name" content="${escapeHtmlUnsafe(value)}"/>"""
|
||||
|
||||
private val tupledTag = (tag _).tupled
|
||||
|
||||
|
@ -37,7 +37,7 @@ case class OpenGraph(
|
|||
object twitter {
|
||||
|
||||
private def tag(name: String, value: String) =
|
||||
s"""<meta name="twitter:$name" content="${escapeHtml(value)}"/>"""
|
||||
s"""<meta name="twitter:$name" content="${escapeHtmlUnsafe(value)}"/>"""
|
||||
|
||||
private val tupledTag = (tag _).tupled
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ searchText = text
|
|||
<tr class="paginated_element">
|
||||
<td>
|
||||
<a class="post" href="@routes.ForumPost.redirect(view.post.id)">@view.categ.name - @view.topic.name #@view.post.number</a>
|
||||
<p>@shortenWithBr(view.post.text, 200)</p>
|
||||
<p>@lila.common.String.html.shortenWithBr(view.post.text, 200)</p>
|
||||
</td>
|
||||
<td class="info">
|
||||
@momentFromNow(view.post.createdAt) by @authorLink(view.post)
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
@tour.spotlight.map { s =>
|
||||
<div class="game_infos spotlight">
|
||||
@markdownLinks(s.description)
|
||||
@lila.common.String.html.markdownLinks(s.description)
|
||||
</div>
|
||||
}
|
||||
@if(verdicts.relevant) {
|
||||
|
@ -52,11 +52,11 @@
|
|||
<br /><br />
|
||||
@trans.winner(): @userIdLink(userId.some)
|
||||
}
|
||||
@if(isGranted(_.TerminateTournament) && tour.isStarted) {
|
||||
@if(isGranted(_.TerminateTournament)) {
|
||||
<form class="terminate" method="post" action="@routes.Tournament.terminate(tour.id)"
|
||||
style="margin-top: 7px; text-align: right;">
|
||||
<button data-icon="j" class="submit text button thin confirm" type="submit"
|
||||
title="Terminates the tournament immediately! Use only if the tournament is killing lichess!">Terminate now</button>
|
||||
title="Terminates the tournament immediately">Terminate now</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -46,7 +46,8 @@ TwirlKeys.templateImports ++= Seq(
|
|||
"lila.app.templating.Environment._",
|
||||
"lila.api.Context",
|
||||
"lila.i18n.{ I18nKeys => trans }",
|
||||
"lila.common.paginator.Paginator"
|
||||
"lila.common.paginator.Paginator",
|
||||
"lila.common.String.html._"
|
||||
)
|
||||
resourceDirectory in Assets := (sourceDirectory in Compile).value / "assets"
|
||||
|
||||
|
|
|
@ -329,7 +329,8 @@ POST /translation/select controllers.I18n.select
|
|||
# Authentication
|
||||
GET /login controllers.Auth.login
|
||||
POST /login controllers.Auth.authenticate
|
||||
GET /logout controllers.Auth.logout
|
||||
GET /logout controllers.Auth.logoutGet
|
||||
POST /logout controllers.Auth.logout
|
||||
GET /signup controllers.Auth.signup
|
||||
POST /signup controllers.Auth.signupPost
|
||||
GET /signup/check-your-email/:name controllers.Auth.checkYourEmail(name: String)
|
||||
|
@ -502,7 +503,7 @@ GET /embed controllers.Main.embed
|
|||
GET /mobile controllers.Main.mobile
|
||||
GET /lag controllers.Main.lag
|
||||
GET /fpmenu controllers.Main.fpmenu
|
||||
GET /fishnet controllers.Main.fishnet
|
||||
GET /get-fishnet controllers.Main.getFishnet
|
||||
|
||||
# Mobile Push
|
||||
POST /mobile/register/:platform/:deviceId controllers.Main.mobileRegister(platform: String, deviceId: String)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit f9ab22441c8e095c149c1e4e68f08cf64428fd8e
|
||||
Subproject commit 01843a5e6cadb0404432e79cb815e06de89764c5
|
|
@ -41,14 +41,96 @@ object String {
|
|||
}
|
||||
}
|
||||
|
||||
// Matches a lichess username with an '@' prefix if it is used as a single
|
||||
// word (i.e. preceded and followed by space or appropriate punctuation):
|
||||
// Yes: everyone says @ornicar is a pretty cool guy
|
||||
// No: contact@lichess.org, @1, http://example.com/@happy0
|
||||
val atUsernameRegex = """(?<=\s|^)@(?>([a-zA-Z_-][\w-]{1,19}))(?![\w-])""".r
|
||||
|
||||
object html {
|
||||
|
||||
private def nl2brUnsafe(text: String): String =
|
||||
text.replace("\r\n", "<br />").replace("\n", "<br />")
|
||||
|
||||
def nl2br(text: String) = Html(nl2brUnsafe(text))
|
||||
|
||||
def shortenWithBr(text: String, length: Int) = Html {
|
||||
nl2brUnsafe(escapeHtmlUnsafe(text).take(length)).replace("<br /><br />", "<br />")
|
||||
}
|
||||
|
||||
def shorten(text: String, length: Int, sep: String = "…"): Html = {
|
||||
val t = text.replace("\n", " ")
|
||||
if (t.size > (length + sep.size)) Html(escapeHtmlUnsafe(t take length) ++ sep)
|
||||
else escapeHtml(t)
|
||||
}
|
||||
|
||||
def autoLink(text: String): Html = nl2br(addUserProfileLinksUnsafe(addLinksUnsafe(escapeHtmlUnsafe(text))))
|
||||
private val urlRegex = """(?i)\b((https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,6}\/)((?:[`!\[\]{};:'".,<>?«»“”‘’]*[^\s`!\[\]{}\(\);:'".,<>?«»“”‘’])*))""".r
|
||||
// private val imgRegex = """(?:(?:https?:\/\/))[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/=]*(\.jpg|\.png|\.jpeg))""".r
|
||||
private val netDomain = "lichess.org" // whatever...
|
||||
|
||||
/**
|
||||
* Creates hyperlinks to user profiles mentioned using the '@' prefix. e.g. @ornicar
|
||||
* @param text The text to regex match
|
||||
* @return The text as a HTML hyperlink
|
||||
*/
|
||||
def addUserProfileLinks(text: String) = Html(addUserProfileLinksUnsafe(text))
|
||||
|
||||
private def addUserProfileLinksUnsafe(text: String): String =
|
||||
atUsernameRegex.replaceAllIn(text, m => {
|
||||
val user = m group 1
|
||||
val url = s"//$netDomain/@/$user"
|
||||
|
||||
s"""<a href="$url">@$user</a>"""
|
||||
})
|
||||
|
||||
def addLinks(text: String) = Html(addLinksUnsafe(text))
|
||||
|
||||
private def addLinksUnsafe(text: String): String = try {
|
||||
urlRegex.replaceAllIn(text, m => {
|
||||
if (m.group(0) contains """) m.group(0)
|
||||
else if (m.group(2) == "http://" || m.group(2) == "https://") {
|
||||
if (s"${m.group(3)}/" startsWith s"$netDomain/") {
|
||||
// internal
|
||||
val link = m.group(3)
|
||||
s"""<a rel="nofollow" href="//$link">${urlOrImgUnsafe(link)}</a>"""
|
||||
} else {
|
||||
// external
|
||||
val link = m.group(1)
|
||||
s"""<a rel="nofollow" href="$link" target="_blank">${urlOrImgUnsafe(link)}</a>"""
|
||||
}
|
||||
} else {
|
||||
if (s"${m.group(2)}/" startsWith s"$netDomain/") {
|
||||
// internal
|
||||
val link = m.group(1)
|
||||
s"""<a rel="nofollow" href="//$link">${urlOrImgUnsafe(link)}</a>"""
|
||||
} else {
|
||||
// external
|
||||
val link = m.group(1)
|
||||
s"""<a rel="nofollow" href="http://$link" target="_blank">${urlOrImgUnsafe(link)}</a>"""
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
case e: IllegalArgumentException =>
|
||||
lila.log("templating").error(s"addLinks($text)", e)
|
||||
text
|
||||
}
|
||||
|
||||
private val imgUrlPattern = """.*\.(jpg|jpeg|png|gif)$""".r.pattern
|
||||
|
||||
private def urlToImgUnsafe(url: String): Option[String] = {
|
||||
imgUrlPattern.matcher(url).matches && !url.contains(s"://$netDomain")
|
||||
} option s"""<img class="embed" src="$url"/>"""
|
||||
|
||||
private def urlOrImgUnsafe(url: String) = urlToImgUnsafe(url) getOrElse url
|
||||
|
||||
// from https://github.com/android/platform_frameworks_base/blob/d59921149bb5948ffbcb9a9e832e9ac1538e05a0/core/java/android/text/TextUtils.java#L1361
|
||||
def escape(s: String): Html = Html(escapeUnsafe(s))
|
||||
def escapeHtml(s: String): Html = Html(escapeHtmlUnsafe(s))
|
||||
|
||||
private val badChars = "[<>&\"']".r.pattern
|
||||
|
||||
def escapeUnsafe(s: String): String = {
|
||||
def escapeHtmlUnsafe(s: String): String = {
|
||||
if (badChars.matcher(s).find) {
|
||||
val sb = new StringBuilder(s.size + 10) // wet finger style
|
||||
var i = 0
|
||||
|
@ -68,5 +150,13 @@ object String {
|
|||
sb.toString
|
||||
} else s
|
||||
}
|
||||
|
||||
private val markdownLinkRegex = """\[([^\[]+)\]\(([^\)]+)\)""".r
|
||||
|
||||
def markdownLinks(text: String): Html = nl2br {
|
||||
markdownLinkRegex.replaceAllIn(escapeHtmlUnsafe(text), m => {
|
||||
s"""<a href="${m group 2}">${m group 1}</a>"""
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package lila.common
|
||||
|
||||
import org.specs2.mutable.Specification
|
||||
import play.twirl.api.Html
|
||||
|
||||
class HtmlTest extends Specification {
|
||||
|
||||
import String.html._
|
||||
|
||||
"add links" should {
|
||||
"detect link" in {
|
||||
val url = "http://zombo.com"
|
||||
addLinks(s"""link to $url here""") must_== Html {
|
||||
s"""link to <a rel="nofollow" href="$url" target="_blank">$url</a> here"""
|
||||
}
|
||||
}
|
||||
"detect image" in {
|
||||
val url = "http://zombo.com/pic.jpg"
|
||||
addLinks(s"""img to $url here""") must_== Html {
|
||||
val img = s"""<img src="$url" width="100%" style="max-width:100%" />"""
|
||||
s"""img to <a rel="nofollow" href="$url" target="_blank">$img</a> here"""
|
||||
}
|
||||
}
|
||||
// "skip markdown images" in {
|
||||
// val url = "http://zombo.com"
|
||||
// addLinks(s"""img of ![some alt]($url) here""") must_== Html {
|
||||
// s"""img of ![some alt]($url) here"""
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -54,6 +54,6 @@ final class MentionNotifier(notifyApi: NotifyApi, relationApi: RelationApi) {
|
|||
}
|
||||
|
||||
private def extractMentionedUsers(post: Post): Set[String] = {
|
||||
User.atUsernameRegex.findAllMatchIn(post.text).map(_.matched.tail).toSet
|
||||
lila.common.String.atUsernameRegex.findAllMatchIn(post.text).map(_.matched.tail).toSet
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package lila.i18n
|
|||
|
||||
import play.twirl.api.Html
|
||||
|
||||
import lila.common.String.html.{ escape => escapeHtml }
|
||||
import lila.common.String.html.escapeHtml
|
||||
|
||||
private sealed trait Translation
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package lila.i18n
|
|||
import play.api.i18n.Lang
|
||||
import play.twirl.api.Html
|
||||
|
||||
import lila.common.String.html.{ escape => escapeHtml }
|
||||
import lila.common.String.html.escapeHtml
|
||||
|
||||
object Translator {
|
||||
|
||||
|
|
|
@ -4,9 +4,7 @@ import play.api.libs.json._
|
|||
|
||||
import actorApi.Member
|
||||
import chess.Color
|
||||
import lila.common.ApiVersion
|
||||
import lila.game.Event
|
||||
import lila.common.String.html.{ escape => escapeHtml }
|
||||
|
||||
case class VersionedEvent(
|
||||
version: Int,
|
||||
|
|
|
@ -32,11 +32,13 @@ blumpkin
|
|||
bollock
|
||||
boner
|
||||
boob
|
||||
bot
|
||||
bugger
|
||||
buk?kake
|
||||
bull?shit
|
||||
cancer
|
||||
cawk
|
||||
cheat(er|)
|
||||
chess(|-|_)bot(.?com)?
|
||||
chink
|
||||
choad
|
||||
|
@ -56,6 +58,7 @@ dogg?ystyle
|
|||
dong
|
||||
douche(bag|)
|
||||
dyke
|
||||
engine
|
||||
(f|ph)ag
|
||||
(f|ph)agg?ot
|
||||
fanny
|
||||
|
@ -78,7 +81,7 @@ horny
|
|||
humping
|
||||
idiot
|
||||
incest
|
||||
jerks?
|
||||
jerk
|
||||
jizz?(um|)
|
||||
kaffir
|
||||
kike
|
||||
|
@ -144,11 +147,10 @@ spunk
|
|||
stfu
|
||||
stripper
|
||||
stupid
|
||||
suc?k
|
||||
taint
|
||||
tart
|
||||
terrorist
|
||||
tit(s|ies|ties|ty)(fuc?k)
|
||||
tit(|t?ies|ty)(fuc?k)
|
||||
tosser
|
||||
turd
|
||||
twat
|
||||
|
|
|
@ -46,7 +46,7 @@ abstract class SocketActor[M <: SocketMember](uidTtl: Duration) extends Socket w
|
|||
// generic message handler
|
||||
def receiveGeneric: Receive = {
|
||||
|
||||
case Ping(uid, None, lagCentis) => ping(uid, lagCentis)
|
||||
case Ping(uid, _, lagCentis) => ping(uid, lagCentis)
|
||||
|
||||
case Broom => broom
|
||||
|
||||
|
@ -87,11 +87,11 @@ abstract class SocketActor[M <: SocketMember](uidTtl: Duration) extends Socket w
|
|||
def ping(uid: String, lagCentis: Option[Centis]) {
|
||||
setAlive(uid)
|
||||
withMember(uid) { member =>
|
||||
member push pong
|
||||
for {
|
||||
lc <- lagCentis
|
||||
user <- member.userId
|
||||
} UserLagCache.put(user, lc)
|
||||
member push pong
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -167,6 +167,11 @@ final class TournamentApi(
|
|||
}
|
||||
}
|
||||
|
||||
def kill(tour: Tournament) {
|
||||
if (tour.isStarted) finish(tour)
|
||||
else if (tour.isCreated) wipe(tour)
|
||||
}
|
||||
|
||||
private def awardTrophies(tour: Tournament): Funit =
|
||||
tour.schedule.??(_.freq == Schedule.Freq.Marathon) ?? {
|
||||
PlayerRepo.bestByTourWithRank(tour.id, 100).flatMap {
|
||||
|
|
|
@ -134,18 +134,12 @@ object User {
|
|||
}
|
||||
implicit def playTimeHandler = reactivemongo.bson.Macros.handler[PlayTime]
|
||||
|
||||
// Matches a lichess username with an '@' prefix if it is used as a single
|
||||
// word (i.e. preceded and followed by space or appropriate punctuation):
|
||||
// Yes: everyone says @ornicar is a pretty cool guy
|
||||
// No: contact@lichess.org, @1, http://example.com/@happy0
|
||||
val atUsernameRegex = """(?<=\s|^)@(?>([a-zA-Z_-][\w-]{1,19}))(?![\w-])""".r
|
||||
|
||||
// what existing usernames are like
|
||||
val historicalUsernameRegex = """(?i)[\w-]*[a-z0-9]""".r
|
||||
val historicalUsernameRegex = """(?i)[a-z0-9][\w-]*[a-z0-9]""".r
|
||||
// what new usernames should be like
|
||||
val newUsernameRegex = """(?i)[a-z][\w-]*[a-z0-9]""".r
|
||||
|
||||
def couldBeUsername(str: String) = historicalUsernameRegex.pattern.matcher(str).matches
|
||||
def couldBeUsername(str: User.ID) = historicalUsernameRegex.pattern.matcher(str).matches
|
||||
|
||||
def normalize(username: String) = username.toLowerCase
|
||||
|
||||
|
|
|
@ -3,15 +3,26 @@ package lila.user
|
|||
import org.specs2.mutable.Specification
|
||||
|
||||
class UserTest extends Specification {
|
||||
|
||||
def canSignup(str: User.ID) =
|
||||
User.newUsernameRegex.pattern.matcher(str).matches
|
||||
|
||||
"username regex" in {
|
||||
import User.couldBeUsername
|
||||
"bad prefix" in {
|
||||
couldBeUsername("000") must beFalse
|
||||
couldBeUsername("0foo") must beFalse
|
||||
"bad prefix: can login" in {
|
||||
couldBeUsername("000") must beTrue
|
||||
couldBeUsername("0foo") must beTrue
|
||||
couldBeUsername("_foo") must beFalse
|
||||
couldBeUsername("-foo") must beFalse
|
||||
}
|
||||
|
||||
"bad prefix: cannot signup" in {
|
||||
canSignup("000") must beFalse
|
||||
canSignup("0foo") must beFalse
|
||||
canSignup("_foo") must beFalse
|
||||
canSignup("-foo") must beFalse
|
||||
}
|
||||
|
||||
"bad suffix" in {
|
||||
couldBeUsername("a_") must beFalse
|
||||
couldBeUsername("a_") must beFalse
|
||||
|
@ -24,4 +35,4 @@ class UserTest extends Specification {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><defs><filter id="shadow-3" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="#ffffff" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="13" dy="17" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><path d="M0 0h512v512H0z" fill="#d0021b" opacity="0"></path><g class="" transform="translate(0,0)" style="touch-action: none;"><path fill="#3893e8" d="M150.25 19.97c-114.48-.574-139.972 184.95 20.563 212.124-29.5.534-55.382 8.11-91.75 25.97C-19.2 306.313.665 462.966 100.874 446c34.48-5.838 51.21-50.325.875-65.375 16.515 29.61-27.968 47.1-41.906 1.938-11.262-36.49 21.145-74.914 52.468-85 30.5-9.82 55.244-10.86 82.47-5.844-36.585 34.247-56.547 80.465-42.376 123.624 44.522 135.595 192.146 82.52 162.844-6.72-10.346-31.506-41.408-46.505-68-10.155 35.164-8.854 50.45 38.75 18.188 49.342-26.355 8.655-60.212-13.527-66.032-41.343-7.82-37.39 19.77-77.195 54.78-95.25 22.176 35.37 38.812 48.68 83.22 72.186 85.843 45.436 212.957-36.54 143.906-110.53-22.626-24.244-54.574-30.02-67.5 13.124 30.188-20.09 60.748 26.8 33.875 47.563-21.95 16.96-61.503 19.135-86.437 5.5-30.797-16.842-53.79-37.798-70.188-66.532 57.07 13.69 119.584-1.065 143-45.342 45.72-86.45-7.046-152.467-59.125-153.375-20.378-.356-40.654 9.237-54.875 31.5-17.85 27.946-9.815 61.533 35.157 59.124-29.11-21.628-1.9-63.623 26.717-45.343 23.378 14.932 22.494 51.88 9.75 77.28-15.165 30.23-60.573 50.738-95.062 24.657-3.008-5.71-5.563-11.683-7.78-17.843 8.99-6.49 14.874-17.028 14.874-28.875 0-17.772-13.252-32.64-30.345-35.218-9.763-47.134-23.34-92.648-84.844-112.594-13.64-4.424-26.437-6.472-38.28-6.53zm117.844 137.405c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.473 16.937-16.936 16.937-9.463 0-16.906-7.474-16.906-16.938 0-9.463 7.443-16.937 16.906-16.937zm-65.406 10.5c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.474 16.937-16.938 16.937-9.463 0-16.937-7.474-16.937-16.938 0-9.463 7.474-16.937 16.938-16.937z"></path></g><!-- react-empty: 13 --></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><path d="M0 0h512v512H0z" fill="transparent" opacity="0"></path><g class="" transform="translate(0,0)" style="touch-action: none;"><path fill="#3893e8" d="M150.25 19.97c-114.48-.574-139.972 184.95 20.563 212.124-29.5.534-55.382 8.11-91.75 25.97C-19.2 306.313.665 462.966 100.874 446c34.48-5.838 51.21-50.325.875-65.375 16.515 29.61-27.968 47.1-41.906 1.938-11.262-36.49 21.145-74.914 52.468-85 30.5-9.82 55.244-10.86 82.47-5.844-36.585 34.247-56.547 80.465-42.376 123.624 44.522 135.595 192.146 82.52 162.844-6.72-10.346-31.506-41.408-46.505-68-10.155 35.164-8.854 50.45 38.75 18.188 49.342-26.355 8.655-60.212-13.527-66.032-41.343-7.82-37.39 19.77-77.195 54.78-95.25 22.176 35.37 38.812 48.68 83.22 72.186 85.843 45.436 212.957-36.54 143.906-110.53-22.626-24.244-54.574-30.02-67.5 13.124 30.188-20.09 60.748 26.8 33.875 47.563-21.95 16.96-61.503 19.135-86.437 5.5-30.797-16.842-53.79-37.798-70.188-66.532 57.07 13.69 119.584-1.065 143-45.342 45.72-86.45-7.046-152.467-59.125-153.375-20.378-.356-40.654 9.237-54.875 31.5-17.85 27.946-9.815 61.533 35.157 59.124-29.11-21.628-1.9-63.623 26.717-45.343 23.378 14.932 22.494 51.88 9.75 77.28-15.165 30.23-60.573 50.738-95.062 24.657-3.008-5.71-5.563-11.683-7.78-17.843 8.99-6.49 14.874-17.028 14.874-28.875 0-17.772-13.252-32.64-30.345-35.218-9.763-47.134-23.34-92.648-84.844-112.594-13.64-4.424-26.437-6.472-38.28-6.53zm117.844 137.405c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.473 16.937-16.936 16.937-9.463 0-16.906-7.474-16.906-16.938 0-9.463 7.443-16.937 16.906-16.937zm-65.406 10.5c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.474 16.937-16.938 16.937-9.463 0-16.937-7.474-16.937-16.938 0-9.463 7.474-16.937 16.938-16.937z" transform="translate(512, 0) scale(-1, 1) rotate(0, 256, 256)"></path></g><!-- react-empty: 6 --></svg>
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -411,10 +411,22 @@ body.piece_letter .fork move {
|
|||
}
|
||||
|
||||
@media (min-width: 1050px) {
|
||||
div.lichess_game div.lichess_ground {
|
||||
|
||||
#puzzle div.lichess_game div.lichess_ground {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
}
|
||||
#puzzle .fork { left: 90px; }
|
||||
|
||||
div.analyse div.lichess_game div.lichess_ground {
|
||||
width: 25vw;
|
||||
min-width: 25vw;
|
||||
}
|
||||
.fork { left: 90px; }
|
||||
div.analyse .fork { left: 90px; }
|
||||
|
||||
.pocket piece {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer_box {
|
||||
|
@ -1201,8 +1213,8 @@ body.dark .aclock.active {
|
|||
font-size: 13.5px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.tview2.literal {
|
||||
padding: 7px 0px 7px 5px;
|
||||
.tview2.inline {
|
||||
padding: 7px 0px 7px 7px;
|
||||
font-size: 18px;
|
||||
line-height: 1.05em;
|
||||
}
|
||||
|
@ -1220,7 +1232,7 @@ body.piece_letter .tview2 move {
|
|||
font-size: 1.092em;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.tview2.literal move {
|
||||
.tview2.inline move {
|
||||
padding: 0.25em 0.17em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -1242,7 +1254,7 @@ body.piece_letter .tview2 move {
|
|||
background: #3893e8;
|
||||
color: #fff!important;
|
||||
}
|
||||
.tview2.literal move:hover {
|
||||
.tview2.inline move:hover {
|
||||
border-radius: 3px;
|
||||
}
|
||||
.tview2 move.empty {
|
||||
|
@ -1263,7 +1275,7 @@ body.piece_letter .tview2 move {
|
|||
.tview2.column move index:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
.tview2.literal move index {
|
||||
.tview2.inline move index {
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
.tview2 line move {
|
||||
|
@ -1311,20 +1323,22 @@ body.piece_letter .tview2 move {
|
|||
color: #c0c0c0;
|
||||
}
|
||||
.tview2 > interrupt {
|
||||
flex: 0 0 100%;
|
||||
font-size: 95%;
|
||||
}
|
||||
.tview2.column > interrupt {
|
||||
flex: 0 0 100%;
|
||||
background: #f8f8f8;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
}
|
||||
.tview2 > interrupt > comment {
|
||||
.tview2.column > interrupt > comment {
|
||||
display: block;
|
||||
padding: 3px 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.tview2.literal comment {
|
||||
.tview2.inline comment {
|
||||
vertical-align: 0.62em;
|
||||
word-wrap: break-word;
|
||||
color: #7492bf;
|
||||
|
@ -1378,10 +1392,13 @@ body.piece_letter .tview2 move {
|
|||
}
|
||||
.tview2 lines {
|
||||
display: block;
|
||||
padding-top: 2px;
|
||||
margin-left: 5px;
|
||||
margin-top: 2px;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 0.8em;
|
||||
border-left: 1px solid #ccc;
|
||||
border-left: 2px solid #ccc;
|
||||
}
|
||||
.tview2 > interrupt > lines {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.tview2 lines.single {
|
||||
border-left: none;
|
||||
|
@ -1391,7 +1408,7 @@ body.piece_letter .tview2 move {
|
|||
}
|
||||
.tview2 line {
|
||||
display: block;
|
||||
padding-left: 4px;
|
||||
padding-left: 7px;
|
||||
}
|
||||
.tview2.column line {
|
||||
margin: 2px 0;
|
||||
|
@ -1404,20 +1421,19 @@ body.piece_letter .tview2 move {
|
|||
}
|
||||
.tview2 lines lines::before {
|
||||
content: ' ';
|
||||
border-top: 1px solid #c0c0c0;
|
||||
border-top: 2px solid #ccc;
|
||||
position: absolute;
|
||||
margin-left: -5.5px;
|
||||
margin-top: -2px;
|
||||
width: 5px;
|
||||
margin-left: -11px;
|
||||
width: 9px;
|
||||
height: 6px;
|
||||
}
|
||||
.tview2 lines line::before {
|
||||
margin-top: 0.7em;
|
||||
margin-left: -4px;
|
||||
margin-top: 0.65em;
|
||||
margin-left: -8px;
|
||||
content: ' ';
|
||||
border-top: 1px solid #c0c0c0;
|
||||
border-top: 2px solid #ccc;
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
width: 8px;
|
||||
height: 6px;
|
||||
}
|
||||
.tview2 lines lines:last-child {
|
||||
|
@ -1443,8 +1459,8 @@ body.piece_letter .tview2 move {
|
|||
content: ')';
|
||||
margin-right: 2px;
|
||||
}
|
||||
.tview2.literal inline::before,
|
||||
.tview2.literal inline::after {
|
||||
.tview2.inline inline::before,
|
||||
.tview2.inline inline::after {
|
||||
vertical-align: 0.7em;
|
||||
}
|
||||
.tview2 .conceal {
|
||||
|
@ -1468,12 +1484,13 @@ body.dark .tview2.column comment {
|
|||
body.dark .tview2.column line comment {
|
||||
color: #888;
|
||||
}
|
||||
body.dark .tview2.literal comment {
|
||||
body.dark .tview2.inline comment {
|
||||
color: #869fc3;
|
||||
}
|
||||
body.dark .tview2 lines,
|
||||
body.dark .tview2 lines lines::before,
|
||||
body.dark .tview2 lines line::before {
|
||||
border-color: #606060;
|
||||
border-color: #666;
|
||||
}
|
||||
.lichess_ground .areplay .result,
|
||||
.lichess_ground .areplay .status {
|
||||
|
|
|
@ -1901,6 +1901,12 @@ iframe.video {
|
|||
border: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
img.embed {
|
||||
max-height: 400px;
|
||||
max-width: 100%;
|
||||
background: rgba(127, 127, 127, 0.05);
|
||||
box-shadow: 0 0 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
body ::-webkit-scrollbar,
|
||||
body ::-webkit-scrollbar-corner {
|
||||
width: 8px;
|
||||
|
|
|
@ -139,8 +139,8 @@ body.dark .lichess_ground .areplay,
|
|||
body.dark .lichess_ground .result,
|
||||
body.dark .lichess_ground .replay .buttons,
|
||||
body.dark .lichess_ground .replay .moves,
|
||||
body.dark .tview2 > index,
|
||||
body.dark .tview2 > index + move,
|
||||
body.dark .tview2.column > index,
|
||||
body.dark .tview2.column > index + move,
|
||||
body.dark div.analysis_menu,
|
||||
body.dark div.analysis_menu > a,
|
||||
body.dark .explorer_box,
|
||||
|
@ -184,8 +184,8 @@ body.dark div.mchat .chat_tabs .tab,
|
|||
body.dark #team table.requests.for-team {
|
||||
border-color: #3d3d3d;
|
||||
}
|
||||
body.dark .tview2 > index,
|
||||
body.dark .tview2 > interrupt,
|
||||
body.dark .tview2.column > index,
|
||||
body.dark .tview2.column > interrupt,
|
||||
body.dark .lichess_ground .areplay .result,
|
||||
body.dark .lichess_ground .areplay .status,
|
||||
body.dark .explorer_box .config .choices span:hover,
|
||||
|
@ -204,7 +204,6 @@ body.dark #modal-wrap .close:hover {
|
|||
|
||||
body.dark hr,
|
||||
body.dark .fork move,
|
||||
body.dark .tview2 lines,
|
||||
body.dark #adv_chart_loader,
|
||||
body.dark .pull-quote p,
|
||||
body.dark .pull-quote p:after,
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
padding: 5px 0;
|
||||
}
|
||||
#dasher_app .links a,
|
||||
#dasher_app .links button,
|
||||
#dasher_app .subs .sub {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
|
@ -17,11 +18,18 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
#dasher_app .links a:hover,
|
||||
#dasher_app .links button:hover,
|
||||
#dasher_app .subs .sub:hover,
|
||||
#dasher_app .langs form > *:hover {
|
||||
background: #F0F0F0;
|
||||
color: #444;
|
||||
}
|
||||
#dasher_app .links button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
#dasher_app .subs {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
@ -400,12 +408,16 @@ body.dark #dasher_app .board .zoom {
|
|||
}
|
||||
body.dark #dasher_app .head,
|
||||
body.dark #dasher_app .links a:hover,
|
||||
body.dark #dasher_app .links button:hover,
|
||||
body.dark #dasher_app .subs a:hover,
|
||||
body.dark #dasher_app .langs form > *:hover,
|
||||
body.dark #dasher_app .selector a:hover {
|
||||
background: #3e3e3e;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
body.dark #dasher_app .links button {
|
||||
color: #8f8f8f;
|
||||
}
|
||||
body.dark #dasher_app .selector a:hover::before {
|
||||
color: #555;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
.gb_edit .gamebook .legend {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ccc;
|
||||
min-height: 3.5em;
|
||||
}
|
||||
.gb_edit .gamebook .legend i {
|
||||
flex: 0 0 50px;
|
||||
|
@ -54,6 +55,8 @@
|
|||
.gb_edit .gamebook .legend p {
|
||||
flex: 1 1 100%;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.gb_edit .gamebook .todo i {
|
||||
background: #dc322f;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.gb_play .lichess_game .lichess_ground {
|
||||
margin-bottom: -140px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.gb_play .gamebook {
|
||||
height: 100%;
|
||||
|
@ -21,13 +21,13 @@
|
|||
position: absolute;
|
||||
content: '';
|
||||
bottom: -9px;
|
||||
left: 40%;
|
||||
right: 20%;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #fff;
|
||||
border-right: 1.5px solid #aaa;
|
||||
border-bottom: 1px solid #aaa;
|
||||
transform: skew(-45deg) rotate(45deg);
|
||||
border-right: 1px solid #aaa;
|
||||
border-bottom: 1.5px solid #aaa;
|
||||
transform: skew(45deg) rotate(45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
.gb_play .comment .content {
|
||||
|
@ -44,13 +44,17 @@
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
.gb_play .floor {
|
||||
margin-top: 20px;
|
||||
flex: 0 0 120px;
|
||||
display: flex;
|
||||
}
|
||||
.gb_play .mascot {
|
||||
margin: 10px 0 -7px 0;
|
||||
cursor: pointer;
|
||||
flex: 0 0 120px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.gb_play .feedback {
|
||||
flex: 0 0 120px;
|
||||
margin-top: 20px;
|
||||
flex: 1 1 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
|
@ -61,7 +65,7 @@
|
|||
background: #e0e0e0;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
.gb_play .feedback.init {
|
||||
.gb_play .feedback.good.init {
|
||||
visibility: hidden;
|
||||
}
|
||||
.gb_play .feedback.act {
|
||||
|
@ -88,6 +92,7 @@
|
|||
.gb_play .feedback.end {
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.gb_play .feedback.end a {
|
||||
background: rgba(56,147,232,0.8);
|
||||
|
@ -103,7 +108,7 @@
|
|||
transition: 0.13s;
|
||||
}
|
||||
.gb_play .feedback.end a::before {
|
||||
font-size: 2em;
|
||||
font-size: 2.5em;
|
||||
margin: 12px 0 8px 0;
|
||||
}
|
||||
.gb_play .feedback.end a:first-child {
|
||||
|
|
|
@ -108,8 +108,8 @@ div.underboard .notif.error {
|
|||
}
|
||||
.study_comment_form form.material.form textarea,
|
||||
.chapter_desc_form form.material.form textarea {
|
||||
height: 80px;
|
||||
font-size: 1.2em;
|
||||
height: 100px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.3em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
@ -128,7 +128,8 @@ div.underboard .notif.error {
|
|||
word-wrap: break-word;
|
||||
display: inline-block;
|
||||
}
|
||||
.study_comments .text a {
|
||||
.study_comments .text a,
|
||||
.chapter_desc .text a {
|
||||
color: #3893E8!important;
|
||||
}
|
||||
.study_comments .edit {
|
||||
|
|
|
@ -33,7 +33,7 @@ body.transp::before {
|
|||
z-index: -1;
|
||||
}
|
||||
body.transp #timeline,
|
||||
body.dark .tview2,
|
||||
body.dark .areplay,
|
||||
body.transp div.training .box,
|
||||
body.transp div.side_box,
|
||||
body.transp div.box,
|
||||
|
@ -73,6 +73,7 @@ body.transp form.translation_form div.message,
|
|||
body.transp #video_side .tag_list a.checked,
|
||||
body.transp #powerTip,
|
||||
body.transp .study_comment_form .material.form .form-group textarea,
|
||||
body.transp .chapter_desc,
|
||||
body.transp div.vstext,
|
||||
body.transp .action_menu {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
|
@ -187,3 +188,6 @@ body.transp ::-webkit-scrollbar-corner {
|
|||
body.transp .action_menu .setting {
|
||||
margin-right: 10px;
|
||||
}
|
||||
body.transp #analyse-cm {
|
||||
color: #888;
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 356b53cc6a45263956df758877af72eff16a9437
|
||||
Subproject commit 1783d134e76e61bb1a64c1d3860740dadee0cbcd
|
|
@ -13,7 +13,7 @@ export const modeChoices = [
|
|||
['normal', "Normal analysis"],
|
||||
['practice', "Practice with computer"],
|
||||
['conceal', "Hide next moves"],
|
||||
['gamebook', "Gamebook: interactive lesson [BETA]"]
|
||||
['gamebook', "Interactive lesson"]
|
||||
];
|
||||
|
||||
export function fieldValue(e: Event, id: string) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import AnalyseCtrl from '../../ctrl';
|
||||
import { path as treePath, ops as treeOps } from 'tree';
|
||||
import Mascot from './mascot';
|
||||
import { makeShapesFromUci } from '../../autoShape';
|
||||
|
||||
type Feedback = 'play' | 'good' | 'bad' | 'end';
|
||||
|
@ -15,7 +14,6 @@ export interface State {
|
|||
|
||||
export default class GamebookPlayCtrl {
|
||||
|
||||
mascot = new Mascot();
|
||||
state: State;
|
||||
|
||||
constructor(readonly root: AnalyseCtrl, readonly chapterId: string, readonly redraw: () => void) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { State } from './gamebookPlayCtrl';
|
|||
const defaultComments = {
|
||||
play: 'What would you play in this position?',
|
||||
bad: 'That\'s not the right move.',
|
||||
end: 'Congratulations! You completed this gamebook.'
|
||||
end: 'Congratulations! You completed this lesson.'
|
||||
};
|
||||
|
||||
export function render(ctrl: GamebookPlayCtrl): VNode {
|
||||
|
@ -23,16 +23,16 @@ export function render(ctrl: GamebookPlayCtrl): VNode {
|
|||
h('div.content', { hook: richHTML(comment) }),
|
||||
state.showHint ? h('div.hint', { hook: richHTML(state.hint!) }) : undefined
|
||||
]) : undefined,
|
||||
h('img.mascot', {
|
||||
attrs: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
src: ctrl.mascot.url(),
|
||||
title: 'Click to choose your teacher'
|
||||
},
|
||||
hook: bind('click', ctrl.mascot.switch, ctrl.redraw)
|
||||
}),
|
||||
renderFeedback(ctrl, state)
|
||||
h('div.floor', [
|
||||
renderFeedback(ctrl, state),
|
||||
h('img.mascot', {
|
||||
attrs: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
src: window.lichess.assetUrl('/assets/images/mascot/octopus.svg')
|
||||
}
|
||||
})
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
export default class Mascot {
|
||||
|
||||
private list = [
|
||||
'octopus',
|
||||
'parrot-head',
|
||||
'camel-head',
|
||||
'owl'
|
||||
];
|
||||
|
||||
private storage = window.lichess.storage.make('gamebook.mascot');
|
||||
|
||||
current = this.storage.get() || this.list[0];
|
||||
|
||||
switch = () => {
|
||||
this.current = this.list[(this.list.indexOf(this.current) + 1) % this.list.length];
|
||||
this.storage.set(this.current);
|
||||
}
|
||||
|
||||
url = () => window.lichess.assetUrl(`/assets/images/mascot/${this.current}.svg`);
|
||||
}
|
|
@ -24,7 +24,7 @@ function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes |
|
|||
isMainline: true,
|
||||
withIndex: opts.withIndex
|
||||
}),
|
||||
...renderInlineCommentsOf(ctx, main),
|
||||
...renderInlineCommentsOf(ctx, main, true),
|
||||
h('interrupt', renderLines(ctx, cs.slice(1), {
|
||||
parentPath: opts.parentPath,
|
||||
isMainline: true
|
||||
|
@ -65,7 +65,7 @@ function renderLines(ctx: Ctx, nodes: Tree.Node[], opts: Opts): VNode {
|
|||
|
||||
function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes {
|
||||
const path = opts.parentPath + node.id,
|
||||
comments = renderInlineCommentsOf(ctx, node);
|
||||
comments = renderInlineCommentsOf(ctx, node, true);
|
||||
if (opts.truncate === 0) return [
|
||||
h('move', { attrs: { p: path } }, '[...]')
|
||||
];
|
||||
|
@ -110,8 +110,8 @@ export default function(ctrl: AnalyseCtrl): VNode {
|
|||
showGlyphs: !!ctrl.study || ctrl.showComputer(),
|
||||
showEval: !!ctrl.study || ctrl.showComputer()
|
||||
};
|
||||
const commentTags = renderInlineCommentsOf(ctx, root);
|
||||
return h('div.tview2.literal', {
|
||||
const commentTags = renderInlineCommentsOf(ctx, root, true);
|
||||
return h('div.tview2.inline', {
|
||||
hook: mainHook(ctrl)
|
||||
}, [
|
||||
...commentTags,
|
|
@ -6,17 +6,18 @@ import AnalyseCtrl from '../ctrl';
|
|||
import contextMenu from '../contextMenu';
|
||||
import { MaybeVNodes, ConcealOf } from '../interfaces';
|
||||
import { authorText as commentAuthorText } from '../study/studyComments';
|
||||
import { enrichText, innerHTML } from '../util';
|
||||
import { path as treePath } from 'tree';
|
||||
import column from './columnView';
|
||||
import literate from './literateView';
|
||||
import inline from './inlineView';
|
||||
import { empty, defined, dropThrottle, storedProp, StoredProp } from 'common';
|
||||
|
||||
export interface Ctx {
|
||||
ctrl: AnalyseCtrl;
|
||||
truncateComments: boolean;
|
||||
showComputer: boolean;
|
||||
showGlyphs: boolean;
|
||||
showEval: boolean;
|
||||
truncateComments: boolean;
|
||||
}
|
||||
|
||||
export interface Opts {
|
||||
|
@ -35,7 +36,7 @@ export interface NodeClasses {
|
|||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export type TreeViewKey = 'column' | 'literate';
|
||||
export type TreeViewKey = 'column' | 'inline';
|
||||
|
||||
export interface TreeView {
|
||||
get: StoredProp<TreeViewKey>;
|
||||
|
@ -47,7 +48,7 @@ export function ctrl(): TreeView {
|
|||
return {
|
||||
get: value,
|
||||
toggle() {
|
||||
value(value() === 'column' ? 'literate' : 'column');
|
||||
value(value() === 'column' ? 'inline' : 'column');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ export function ctrl(): TreeView {
|
|||
// entry point, dispatching to selected view
|
||||
export function render(ctrl: AnalyseCtrl, concealOf?: ConcealOf): VNode {
|
||||
if (ctrl.treeView.get() === 'column') return column(ctrl, concealOf);
|
||||
return literate(ctrl);
|
||||
return inline(ctrl);
|
||||
}
|
||||
|
||||
export function nodeClasses(c: AnalyseCtrl, path: Tree.Path): NodeClasses {
|
||||
|
@ -71,12 +72,15 @@ export function nodeClasses(c: AnalyseCtrl, path: Tree.Path): NodeClasses {
|
|||
};
|
||||
}
|
||||
|
||||
export function renderInlineCommentsOf(ctx: Ctx, node: Tree.Node): MaybeVNodes {
|
||||
export function renderInlineCommentsOf(ctx: Ctx, node: Tree.Node, rich?: boolean): MaybeVNodes {
|
||||
if (!ctx.ctrl.showComments || empty(node.comments)) return [];
|
||||
return node.comments!.map(comment => {
|
||||
if (comment.by === 'lichess' && !ctx.showComputer) return;
|
||||
return h('comment', [
|
||||
node.comments![1] ? h('span.by', commentAuthorText(comment.by)) : null,
|
||||
const by = node.comments![1] ? h('span.by', commentAuthorText(comment.by)) : null;
|
||||
return rich ? h('comment', {
|
||||
hook: innerHTML(comment.text, text => enrichText(text, true))
|
||||
}) : h('comment', [
|
||||
by,
|
||||
truncateComment(comment.text, 300, ctx)
|
||||
]);
|
||||
}).filter(nonEmpty);
|
||||
|
|
|
@ -67,8 +67,7 @@ export function plural(noun: string, nb: number): string {
|
|||
|
||||
export function titleNameToId(titleName: string): string {
|
||||
const split = titleName.split(' ');
|
||||
const name = split.length == 1 ? split[0] : split[1];
|
||||
return name.toLowerCase();
|
||||
return (split.length === 1 ? split[0] : split[1]).toLowerCase();
|
||||
}
|
||||
|
||||
export function spinner() {
|
||||
|
@ -95,30 +94,35 @@ export function innerHTML<A>(a: A, toHtml: (a: A) => string): Hooks {
|
|||
}
|
||||
|
||||
export function toYouTubeEmbed(url: string, height: number = 300): string | undefined {
|
||||
let embedUrl = window.lichess.toYouTubeEmbedUrl(url);
|
||||
const embedUrl = window.lichess.toYouTubeEmbedUrl(url);
|
||||
if (embedUrl) return `<iframe width="100%" height="${height}" src="${embedUrl}" frameborder=0 allowfullscreen></iframe>`;
|
||||
}
|
||||
|
||||
const commentYoutubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:.*?(?:[?&]v=)|v\/)|youtu\.be\/)(?:[^"&?\/ ]{11})\b/i;
|
||||
const imgUrlRegex = /\.(jpg|jpeg|png|gif)$/;
|
||||
const newLineRegex = /\n/g;
|
||||
|
||||
function imageTag(url: string): string | undefined {
|
||||
if (imgUrlRegex.test(url)) return `<img src="${url}" class="embed"/>`;
|
||||
}
|
||||
|
||||
function toLink(url: string) {
|
||||
if (commentYoutubeRegex.test(url)) return toYouTubeEmbed(url) || url;
|
||||
const show = imageTag(url) || url.replace(/https?:\/\//, '');
|
||||
return '<a target="_blank" rel="nofollow" href="' + url + '">' + show + '</a>';
|
||||
}
|
||||
|
||||
export function enrichText(text: string, allowNewlines: boolean): string {
|
||||
let html = autolink(window.lichess.escapeHtml(text), url => {
|
||||
if (commentYoutubeRegex.test(url)) {
|
||||
return toYouTubeEmbed(url) || url;
|
||||
}
|
||||
const show = url.replace(/https?:\/\//, '');
|
||||
return '<a target="_blank" rel="nofollow" href="' + url + '">' + show + '</a>';
|
||||
});
|
||||
if (allowNewlines) html = html.replace(/\n/g, '<br>');
|
||||
let html = autolink(window.lichess.escapeHtml(text), toLink);
|
||||
if (allowNewlines) html = html.replace(newLineRegex, '<br>');
|
||||
return html;
|
||||
}
|
||||
|
||||
// from https://github.com/bryanwoods/autolink-js/blob/master/autolink.js
|
||||
const linkRegex = /(^|[\s\n]|<[A-Za-z]*\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
|
||||
|
||||
export function autolink(str: string, callback: (str: string) => string): string {
|
||||
const pattern = /(^|[\s\n]|<[A-Za-z]*\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
|
||||
return str.replace(pattern, function(_, space, url) {
|
||||
return "" + space + callback(url);
|
||||
});
|
||||
return str.replace(linkRegex, (_, space, url) => space + callback(url));
|
||||
}
|
||||
|
||||
export function option(value: string, current: string | undefined, name: string) {
|
||||
|
|
|
@ -172,7 +172,7 @@ export default function(opts: CevalOpts): CevalCtrl {
|
|||
hashSize,
|
||||
infinite,
|
||||
hovering,
|
||||
setHovering(fen: Fen, uci: Uci) {
|
||||
setHovering(fen: Fen, uci?: Uci) {
|
||||
hovering(uci ? {
|
||||
fen,
|
||||
uci
|
||||
|
|
|
@ -65,7 +65,7 @@ export interface CevalCtrl {
|
|||
possible: boolean;
|
||||
isComputing(): boolean;
|
||||
variant: Variant;
|
||||
setHovering: (fen: string, uci: string | null) => void;
|
||||
setHovering: (fen: string, uci?: string) => void;
|
||||
multiPv: StoredProp<number>;
|
||||
start: (path: string, steps: Step[], threatMode: boolean, deeper: boolean) => void;
|
||||
stop(): void;
|
||||
|
|
|
@ -192,17 +192,26 @@ export function renderCeval(ctrl: ParentCtrl): VNode | undefined {
|
|||
class: {
|
||||
computing: percent < 100 && instance.isComputing()
|
||||
}
|
||||
}, [progressBar].concat(body).concat(switchButton).concat(threatButton(ctrl)));
|
||||
}, [
|
||||
progressBar,
|
||||
...body,
|
||||
switchButton,
|
||||
threatButton(ctrl)
|
||||
]);
|
||||
}
|
||||
|
||||
function getElFen(el: HTMLElement): string {
|
||||
return el.getAttribute('data-fen')!;
|
||||
}
|
||||
|
||||
function getElUci(e: MouseEvent): string | undefined {
|
||||
return $(e.target).closest('div.pv').attr('data-uci');
|
||||
}
|
||||
|
||||
function checkHover(el: HTMLElement, instance: CevalCtrl): void {
|
||||
setTimeout(function() {
|
||||
window.lichess.requestIdleCallback(() => {
|
||||
instance.setHovering(getElFen(el), $(el).find('div.pv:hover').attr('data-uci'));
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderPvs(ctrl: ParentCtrl) {
|
||||
|
@ -221,14 +230,14 @@ export function renderPvs(ctrl: ParentCtrl) {
|
|||
hook: {
|
||||
insert: vnode => {
|
||||
const el = vnode.elm as HTMLElement;
|
||||
el.addEventListener('mouseover', function(e) {
|
||||
instance.setHovering(getElFen(el), $(e.target).closest('div.pv').attr('data-uci'));
|
||||
el.addEventListener('mouseover', (e: MouseEvent) => {
|
||||
instance.setHovering(getElFen(el), getElUci(e));
|
||||
});
|
||||
el.addEventListener('mouseout', function() {
|
||||
instance.setHovering(getElFen(el), null);
|
||||
el.addEventListener('mouseout', () => {
|
||||
instance.setHovering(getElFen(el));
|
||||
});
|
||||
el.addEventListener('mousedown', function(e) {
|
||||
const uci = $(e.target).closest('div.pv').attr('data-uci');
|
||||
el.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
const uci = getElUci(e);
|
||||
if (uci) ctrl.playUci(uci);
|
||||
});
|
||||
checkHover(el, instance);
|
||||
|
|
|
@ -184,7 +184,7 @@ export function render(ctrl): VNode {
|
|||
ctrl: ctrl,
|
||||
showComputer: false
|
||||
};
|
||||
return h('div.tview2', {
|
||||
return h('div.tview2.column', {
|
||||
hook: {
|
||||
insert: vnode => {
|
||||
const el = vnode.elm as HTMLElement;
|
||||
|
|
Loading…
Reference in New Issue