personal opening explorer UI WIP

openingexplorer3
Thibault Duplessis 2021-10-18 19:20:52 +02:00
parent ca36d40fce
commit b1a398b66d
19 changed files with 269 additions and 142 deletions

View File

@ -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")

View File

@ -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),

View File

@ -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

View File

@ -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))
)})""")

View File

@ -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,

View File

@ -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
)
)}""")
),

View File

@ -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(

View File

@ -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
)
)}""")
),

View File

@ -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
)

View File

@ -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
)

View File

@ -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"

View File

@ -11,7 +11,7 @@
@import 'layout';
@import 'tools';
@import 'action-menu';
@import 'explorer';
@import 'explorer/explorer';
@import 'training';
@import 'practice';
@import 'fork';

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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<ExplorerSpeed[]>('explorer.speed', () => allSpeeds),
},
playerName: {
open: prop(false),
value: storedProp<string | undefined>('explorer.player.name', document.body.dataset['user'] || ''),
},
};
const toggleMany = function <T>(c: StoredJsonProp<T[]>, 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<HTMLInputElement>(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
)
)
),
]),
]);

View File

@ -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: [],

View File

@ -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: {

View File

@ -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<OpeningData> {
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<OpeningData> {
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');

View File

@ -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<ExplorerSpeed[]>;
};
playerName: {
open: Prop<boolean>;
value: StoredProp<string>;
};
}
export interface ExplorerConfigCtrl {