diff --git a/app/Env.scala b/app/Env.scala index 5aef82d4a8..a15612b831 100644 --- a/app/Env.scala +++ b/app/Env.scala @@ -94,6 +94,7 @@ final class Env( ) { val explorerEndpoint = config.get[String]("explorer.endpoint") + val explorer3Endpoint = config.get[String]("explorer.endpoint3") val tablebaseEndpoint = config.get[String]("explorer.tablebase.endpoint") val appVersionDate = config.getOptional[String]("app.version.date") diff --git a/app/templating/AssetHelper.scala b/app/templating/AssetHelper.scala index ec5b3637dc..edbcea34b4 100644 --- a/app/templating/AssetHelper.scala +++ b/app/templating/AssetHelper.scala @@ -84,7 +84,8 @@ trait AssetHelper { self: I18nHelper with SecurityHelper => } ContentSecurityPolicy( defaultSrc = List("'self'", assets), - connectSrc = "'self'" :: assets :: sockets ::: env.explorerEndpoint :: env.tablebaseEndpoint :: Nil, + connectSrc = + "'self'" :: assets :: sockets ::: env.explorerEndpoint :: env.tablebaseEndpoint :: env.explorer3Endpoint :: Nil, styleSrc = List("'self'", "'unsafe-inline'", assets), frameSrc = List("'self'", assets, "https://www.youtube.com", "https://player.twitch.tv"), workerSrc = List("'self'", assets), diff --git a/app/templating/Environment.scala b/app/templating/Environment.scala index 1dec94f4ca..4faf9dfbf3 100644 --- a/app/templating/Environment.scala +++ b/app/templating/Environment.scala @@ -40,6 +40,7 @@ object Environment def apiVersion = lila.api.Mobile.Api.currentVersion def explorerEndpoint = env.explorerEndpoint + def explorer3Endpoint = env.explorer3Endpoint def tablebaseEndpoint = env.tablebaseEndpoint def isChatPanicEnabled = env.chat.panic.enabled diff --git a/app/views/analyse/replay.scala b/app/views/analyse/replay.scala index 36e5aa30af..374d5fd1f8 100644 --- a/app/views/analyse/replay.scala +++ b/app/views/analyse/replay.scala @@ -94,14 +94,11 @@ object replay { embedJsUnsafeLoadThen(s"""LichessAnalyse.boot(${safeJsonValue( Json .obj( - "data" -> data, - "i18n" -> jsI18n(), - "userId" -> ctx.userId, - "chat" -> chatJson, - "explorer" -> Json.obj( - "endpoint" -> explorerEndpoint, - "tablebaseEndpoint" -> tablebaseEndpoint - ) + "data" -> data, + "i18n" -> jsI18n(), + "userId" -> ctx.userId, + "chat" -> chatJson, + "explorer" -> views.html.board.bits.explorerEndpoints ) .add("hunter" -> isGranted(_.Hunter)) )})""") diff --git a/app/views/board/bits.scala b/app/views/board/bits.scala index 34a25c3e5b..05aa23731c 100644 --- a/app/views/board/bits.scala +++ b/app/views/board/bits.scala @@ -42,6 +42,12 @@ object bits { ) .add("fen" -> fen) + val explorerEndpoints = Json.obj( + "endpoint" -> explorerEndpoint, + "endpoint3" -> explorer3Endpoint, + "tablebaseEndpoint" -> tablebaseEndpoint + ) + private val i18nKeyes = List( trans.setTheBoard, trans.boardEditor, diff --git a/app/views/board/userAnalysis.scala b/app/views/board/userAnalysis.scala index 3de7f34c96..2dab17e0c6 100644 --- a/app/views/board/userAnalysis.scala +++ b/app/views/board/userAnalysis.scala @@ -28,13 +28,10 @@ object userAnalysis { analyseNvuiTag, embedJsUnsafe(s"""lichess.userAnalysis=${safeJsonValue( Json.obj( - "data" -> data, - "i18n" -> userAnalysisI18n(withForecast = withForecast), - "explorer" -> Json.obj( - "endpoint" -> explorerEndpoint, - "tablebaseEndpoint" -> tablebaseEndpoint - ), - "wiki" -> pov.game.variant.standard + "data" -> data, + "i18n" -> userAnalysisI18n(withForecast = withForecast), + "explorer" -> bits.explorerEndpoints, + "wiki" -> pov.game.variant.standard ) )}""") ), diff --git a/app/views/board/userAnalysisI18n.scala b/app/views/board/userAnalysisI18n.scala index 28d858a8f5..2ac68309e6 100644 --- a/app/views/board/userAnalysisI18n.scala +++ b/app/views/board/userAnalysisI18n.scala @@ -165,7 +165,8 @@ object userAnalysisI18n { trans.maybeIncludeMoreGamesFromThePreferencesMenu, trans.winPreventedBy50MoveRule, trans.lossSavedBy50MoveRule, - trans.allSet + trans.allSet, + trans.study.searchByUsername ).map(_.key) private val forecastTranslations: Vector[MessageKey] = Vector( diff --git a/app/views/practice/show.scala b/app/views/practice/show.scala index c65daf43c8..74e29e8f29 100644 --- a/app/views/practice/show.scala +++ b/app/views/practice/show.scala @@ -26,10 +26,7 @@ object show { "study" -> data.study, "data" -> data.analysis, "i18n" -> board.userAnalysisI18n(), - "explorer" -> Json.obj( - "endpoint" -> explorerEndpoint, - "tablebaseEndpoint" -> tablebaseEndpoint - ) + "explorer" -> views.html.board.bits.explorerEndpoints ) )}""") ), diff --git a/app/views/relay/show.scala b/app/views/relay/show.scala index 31b88095b3..25b56362ef 100644 --- a/app/views/relay/show.scala +++ b/app/views/relay/show.scala @@ -44,10 +44,7 @@ object show { localMod = ctx.userId.??(rt.study.canContribute) ) ), - "explorer" -> Json.obj( - "endpoint" -> explorerEndpoint, - "tablebaseEndpoint" -> tablebaseEndpoint - ), + "explorer" -> views.html.board.bits.explorerEndpoints, "socketUrl" -> views.html.study.show.socketUrl(rt.study.id.value), "socketVersion" -> socketVersion.value ) diff --git a/app/views/study/show.scala b/app/views/study/show.scala index 50d9ba77ea..9f4b6a3d61 100644 --- a/app/views/study/show.scala +++ b/app/views/study/show.scala @@ -43,10 +43,7 @@ object show { localMod = ctx.userId exists s.canContribute ) }, - "explorer" -> Json.obj( - "endpoint" -> explorerEndpoint, - "tablebaseEndpoint" -> tablebaseEndpoint - ), + "explorer" -> views.html.board.bits.explorerEndpoints, "socketUrl" -> socketUrl(s.id.value), "socketVersion" -> socketVersion.value ) diff --git a/conf/base.conf b/conf/base.conf index e60fdb785b..41c84d92dd 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -324,6 +324,7 @@ streamer { } explorer { endpoint = "https://explorer.lichess.ovh" + endpoint3 = "https://explorer.lichess.ovh" internal_endpoint = "http://explorer.lichess.ovh" tablebase = { endpoint = "https://tablebase.lichess.ovh" diff --git a/ui/analyse/css/_analyse.base.scss b/ui/analyse/css/_analyse.base.scss index 5b891aa6fe..339ccc1b5a 100644 --- a/ui/analyse/css/_analyse.base.scss +++ b/ui/analyse/css/_analyse.base.scss @@ -11,7 +11,7 @@ @import 'layout'; @import 'tools'; @import 'action-menu'; -@import 'explorer'; +@import 'explorer/explorer'; @import 'training'; @import 'practice'; @import 'fork'; diff --git a/ui/analyse/css/explorer/_config.scss b/ui/analyse/css/explorer/_config.scss new file mode 100644 index 0000000000..bf66643e08 --- /dev/null +++ b/ui/analyse/css/explorer/_config.scss @@ -0,0 +1,89 @@ +.explorer__config { + section { + margin: 0.4em $block-gap 0 $block-gap; + } + + section.save { + text-align: center; + padding: 15px 0 10px 0; + } + + label { + font-weight: bold; + display: block; + line-height: 2em; + } + + .choices { + display: flex; + + button { + @extend %metal; + flex-grow: 1; + padding: 5px 0; + text-align: center; + cursor: pointer; + + @include transition; + + border: $border; + border-width: 1px 0 1px 1px; + text-transform: capitalize; + + &:first-child { + @extend %box-radius-left; + } + + &:last-child { + @extend %box-radius-right; + + border-right-width: 1px; + } + + &:hover { + @extend %metal-hover; + } + + &[aria-pressed='true'] { + background: $c-secondary; + color: $c-secondary-over; + text-shadow: 1px 0 0 rgba(0, 0, 0, 0.5); + font-weight: bold; + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2) inset; + } + } + } + + .name { + @extend %flex-center; + margin-top: 1em; + .user-link { + @extend %box-radius; + font-size: 1.3em; + padding-right: 1em; + + background: mix($c-primary, $c-bg-box, 12%); + padding: 0.2em 0.6em; + margin-right: 0.5em; + } + .button { + width: 100%; + } + } + + &__player__choice { + > div { + // for user complete + overflow: visible !important; + } + + .input-wrapper { + // for user complete + overflow: visible !important; + position: relative; + display: inline-block; + + padding-top: 2em; + } + } +} diff --git a/ui/analyse/css/_explorer.scss b/ui/analyse/css/explorer/_explorer.scss similarity index 81% rename from ui/analyse/css/_explorer.scss rename to ui/analyse/css/explorer/_explorer.scss index 31626024ea..60f623fc0d 100644 --- a/ui/analyse/css/_explorer.scss +++ b/ui/analyse/css/explorer/_explorer.scss @@ -1,3 +1,5 @@ +@import './config'; + .explorer-box { position: relative; flex: 2.5 1 0px; @@ -291,61 +293,4 @@ font-size: 40px; margin: 10px 0; } - - .config { - section { - margin: 0.4em $block-gap 0 $block-gap; - } - - section.save { - text-align: center; - padding: 15px 0 10px 0; - } - - label { - font-weight: bold; - display: block; - line-height: 2em; - } - - .choices { - display: flex; - - button { - @extend %metal; - flex-grow: 1; - padding: 5px 0; - text-align: center; - cursor: pointer; - - @include transition; - - border: $border; - border-width: 1px 0 1px 1px; - text-transform: capitalize; - - &:first-child { - @extend %box-radius-left; - } - - &:last-child { - @extend %box-radius-right; - - border-right-width: 1px; - } - - &:hover { - @extend %metal-hover; - } - - &[aria-pressed='true'] { - background: $c-secondary; - color: $c-secondary-over; - text-shadow: 1px 0 0 rgba(0, 0, 0, 0.5); - font-weight: bold; - box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2) inset; - } - } - } - } } diff --git a/ui/analyse/src/explorer/explorerConfig.ts b/ui/analyse/src/explorer/explorerConfig.ts index 93d93180e2..7a762eb1a7 100644 --- a/ui/analyse/src/explorer/explorerConfig.ts +++ b/ui/analyse/src/explorer/explorerConfig.ts @@ -1,9 +1,10 @@ import { h, VNode } from 'snabbdom'; import { prop } from 'common'; -import { bind, dataIcon } from 'common/snabbdom'; +import { bind, dataIcon, onInsert } from 'common/snabbdom'; import { storedProp, storedJsonProp, StoredJsonProp } from 'common/storage'; import { Game } from '../interfaces'; import { ExplorerDb, ExplorerSpeed, ExplorerConfigData, ExplorerConfigCtrl } from './interfaces'; +import { snabModal } from 'common/modal'; const allSpeeds: ExplorerSpeed[] = ['bullet', 'blitz', 'rapid', 'classical']; const allRatings = [1600, 1800, 2000, 2200, 2500]; @@ -11,11 +12,11 @@ const allRatings = [1600, 1800, 2000, 2200, 2500]; export function controller(game: Game, onClose: () => void, trans: Trans, redraw: () => void): ExplorerConfigCtrl { const variant = game.variant.key === 'fromPosition' ? 'standard' : game.variant.key; - const available: ExplorerDb[] = ['lichess']; + const available: ExplorerDb[] = ['lichess', 'player']; if (variant === 'standard') available.unshift('masters'); const data: ExplorerConfigData = { - open: prop(false), + open: prop(true), db: { available, selected: @@ -33,6 +34,10 @@ export function controller(game: Game, onClose: () => void, trans: Trans, redraw available: allSpeeds, selected: storedJsonProp('explorer.speed', () => allSpeeds), }, + playerName: { + open: prop(false), + value: storedProp('explorer.player.name', document.body.dataset['user'] || ''), + }, }; const toggleMany = function (c: StoredJsonProp, value: T) { @@ -68,69 +73,30 @@ export function controller(game: Game, onClose: () => void, trans: Trans, redraw } export function view(ctrl: ExplorerConfigCtrl): VNode[] { - const d = ctrl.data; return [ h('section.db', [ h('label', ctrl.trans.noarg('database')), h( 'div.choices', - d.db.available.map(function (s) { - return h( + ctrl.data.db.available.map(s => + h( 'button', { attrs: { - 'aria-pressed': `${d.db.selected() === s}`, + 'aria-pressed': `${ctrl.data.db.selected() === s}`, }, hook: bind('click', _ => ctrl.toggleDb(s), ctrl.redraw), }, s - ); - }) + ) + ) ), ]), - d.db.selected() === 'masters' - ? h('div.masters.message', [ - h('i', { attrs: dataIcon('') }), - h('p', ctrl.trans('masterDbExplanation', 2200, '1952', '2019')), - ]) - : h('div', [ - h('section.rating', [ - h('label', ctrl.trans.noarg('averageElo')), - h( - 'div.choices', - d.rating.available.map(function (r) { - return h( - 'button', - { - attrs: { - 'aria-pressed': `${d.rating.selected().includes(r)}`, - }, - hook: bind('click', _ => ctrl.toggleRating(r), ctrl.redraw), - }, - r.toString() - ); - }) - ), - ]), - h('section.speed', [ - h('label', ctrl.trans.noarg('timeControl')), - h( - 'div.choices', - d.speed.available.map(function (s) { - return h( - 'button', - { - attrs: { - 'aria-pressed': `${d.speed.selected().includes(s)}`, - }, - hook: bind('click', _ => ctrl.toggleSpeed(s), ctrl.redraw), - }, - s - ); - }) - ), - ]), - ]), + ctrl.data.db.selected() === 'masters' + ? masterDb(ctrl) + : ctrl.data.db.selected() === 'lichess' + ? lichessDb(ctrl) + : playerDb(ctrl), h( 'section.save', h( @@ -144,3 +110,108 @@ export function view(ctrl: ExplorerConfigCtrl): VNode[] { ), ]; } + +const playerDb = (ctrl: ExplorerConfigCtrl) => { + const name = ctrl.data.playerName.value(); + return h('div.player-db', [ + ctrl.data.playerName.open() ? playerModal(ctrl) : undefined, + h('section.name', [ + name + ? h( + 'span.user-link.ulpt', + { + hook: onInsert(lichess.powertip.manualUser), + attrs: { 'data-href': `/@/${name}` }, + }, + name + ) + : undefined, + h( + `button.${name ? 'button-link' : 'button'}`, + { + hook: bind('click', () => ctrl.data.playerName.open(true), ctrl.redraw), + }, + name ? 'Change' : 'Select a Lichess player' + ), + ]), + ]); +}; + +const playerModal = (ctrl: ExplorerConfigCtrl) => { + return snabModal({ + class: 'explorer__config__player__choice', + onClose() { + ctrl.data.playerName.open(false); + ctrl.redraw(); + }, + content: [ + h('h2', 'Personal opening explorer'), + h('div.input-wrapper', [ + h('input', { + attrs: { placeholder: ctrl.trans.noarg('searchByUsername') }, + hook: onInsert(input => + lichess.userComplete().then(uac => { + uac({ + input, + tag: 'span', + onSelect(v) { + // input.value = v.name; + ctrl.data.playerName.value(v.name); + ctrl.data.playerName.open(false); + ctrl.redraw(); + }, + }); + input.focus(); + }) + ), + }), + ]), + ], + }); +}; + +const masterDb = (ctrl: ExplorerConfigCtrl) => + h('div.masters.message', [ + h('i', { attrs: dataIcon('') }), + h('p', ctrl.trans('masterDbExplanation', 2200, '1952', '2019')), + ]); + +const lichessDb = (ctrl: ExplorerConfigCtrl) => + h('div', [ + h('section.rating', [ + h('label', ctrl.trans.noarg('averageElo')), + h( + 'div.choices', + ctrl.data.rating.available.map(r => + h( + 'button', + { + attrs: { + 'aria-pressed': `${ctrl.data.rating.selected().includes(r)}`, + }, + hook: bind('click', _ => ctrl.toggleRating(r), ctrl.redraw), + }, + r.toString() + ) + ) + ), + ]), + h('section.speed', [ + h('label', ctrl.trans.noarg('timeControl')), + h( + 'div.choices', + ctrl.data.speed.available.map(s => + h( + 'button', + { + attrs: { + 'aria-pressed': `${ctrl.data.speed.selected().includes(s)}`, + }, + hook: bind('click', _ => ctrl.toggleSpeed(s), ctrl.redraw), + }, + s + ) + ) + ), + ]), + ]); diff --git a/ui/analyse/src/explorer/explorerCtrl.ts b/ui/analyse/src/explorer/explorerCtrl.ts index 95ea2af9c5..fe118cfb59 100644 --- a/ui/analyse/src/explorer/explorerCtrl.ts +++ b/ui/analyse/src/explorer/explorerCtrl.ts @@ -78,7 +78,12 @@ export default function (root: AnalyseCtrl, opts: ExplorerOpts, allow: boolean): ? xhr.tablebase(opts.tablebaseEndpoint, effectiveVariant, fen) : xhr.opening({ endpoint: opts.endpoint, + endpoint3: opts.endpoint3, db: config.data.db.selected() as ExplorerDb, + personal: { + player: config.data.playerName.value(), + color: root.getOrientation(), + }, variant: effectiveVariant, rootFen: root.nodeList[0].fen, play: root.nodeList.slice(1).map(s => s.uci!), @@ -180,6 +185,7 @@ export default function (root: AnalyseCtrl, opts: ExplorerOpts, allow: boolean): return xhr .opening({ endpoint: opts.endpoint, + endpoint3: opts.endpoint3, db: 'masters', rootFen: fen, play: [], diff --git a/ui/analyse/src/explorer/explorerView.ts b/ui/analyse/src/explorer/explorerView.ts index 9c4a088d33..94ba4047e3 100644 --- a/ui/analyse/src/explorer/explorerView.ts +++ b/ui/analyse/src/explorer/explorerView.ts @@ -426,7 +426,7 @@ export default function (ctrl: AnalyseCtrl): VNode | undefined { { class: { loading, - config: configOpened, + explorer__config: configOpened, reduced: !configOpened && (!!explorer.failing() || explorer.movesAway() > 2), }, hook: { diff --git a/ui/analyse/src/explorer/explorerXhr.ts b/ui/analyse/src/explorer/explorerXhr.ts index 440d008905..e8084dc275 100644 --- a/ui/analyse/src/explorer/explorerXhr.ts +++ b/ui/analyse/src/explorer/explorerXhr.ts @@ -3,7 +3,12 @@ import * as xhr from 'common/xhr'; interface OpeningXhrOpts { endpoint: string; + endpoint3: string; db: ExplorerDb; + personal?: { + player: string; + color: Color; + }; rootFen: Fen; play: string[]; fen: Fen; @@ -14,7 +19,8 @@ interface OpeningXhrOpts { } export function opening(opts: OpeningXhrOpts): Promise { - const url = new URL(opts.db === 'lichess' ? '/lichess' : '/master', opts.endpoint); + const endpoint = opts.db == 'player' ? opts.endpoint3 : opts.endpoint; + const url = new URL(opts.db === 'lichess' ? '/lichess' : opts.db == 'player' ? '/personal' : '/master', endpoint); const params = url.searchParams; params.set('fen', opts.rootFen); params.set('play', opts.play.join(',')); @@ -23,6 +29,11 @@ export function opening(opts: OpeningXhrOpts): Promise { if (opts.speeds) for (const speed of opts.speeds) params.append('speeds[]', speed); if (opts.ratings) for (const rating of opts.ratings) params.append('ratings[]', rating.toString()); } + if (opts.db === 'player' && opts.personal) { + params.set('player', opts.personal.player); + params.set('color', opts.personal.color); + // params.set('update', 'true'); + } if (!opts.withGames) { params.set('topGames', '0'); params.set('recentGames', '0'); diff --git a/ui/analyse/src/explorer/interfaces.ts b/ui/analyse/src/explorer/interfaces.ts index 57de4b2607..d7f9ed8eec 100644 --- a/ui/analyse/src/explorer/interfaces.ts +++ b/ui/analyse/src/explorer/interfaces.ts @@ -6,12 +6,17 @@ export interface Hovering { uci: Uci; } -export type ExplorerDb = 'lichess' | 'masters'; +export type ExplorerDb = 'lichess' | 'masters' | 'player'; export type ExplorerSpeed = 'bullet' | 'blitz' | 'rapid' | 'classical'; +export interface PlayerOpts { + name: string; +} + export interface ExplorerOpts { endpoint: string; + endpoint3: string; tablebaseEndpoint: string; } @@ -29,6 +34,10 @@ export interface ExplorerConfigData { available: ExplorerSpeed[]; selected: StoredJsonProp; }; + playerName: { + open: Prop; + value: StoredProp; + }; } export interface ExplorerConfigCtrl {