move all round accessibility to a ui/round plugin

pull/4848/head
Thibault Duplessis 2019-01-20 18:57:04 +08:00
parent 9bccda5637
commit bd600f1404
10 changed files with 118 additions and 197 deletions

View File

@ -240,21 +240,6 @@ object Round extends LilaController with TheftPrevention {
}
}
def playerNvui(fullId: String) = Open { implicit ctx =>
OptionFuResult(GameRepo pov fullId)(nvui(true))
}
def watcherNvui(gameId: String, color: String) = Open { implicit ctx =>
OptionFuResult(GameRepo.pov(gameId, color))(nvui(false))
}
private def nvui(playing: Boolean)(pov: Pov)(implicit ctx: Context): Fu[Result] =
if (ctx.blind) negotiate(
html = Ok(html.game.nvui.html(pov, playing)).fuccess,
api = _ => Ok(html.game.nvui.json(pov)).fuccess
)
else BadRequest.fuccess
def sides(gameId: String, color: String) = Open { implicit ctx =>
OptionFuResult(GameRepo.pov(gameId, color)) { pov =>
(pov.game.tournamentId ?? lila.tournament.TournamentRepo.byId) zip

View File

@ -1,72 +0,0 @@
package views.html
package game
import play.api.libs.json._
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.game.Pov
import controllers.routes
object nvui {
def json(pov: Pov)(implicit ctx: Context) = Json.obj(
"pgn" -> pov.game.pgnMoves.mkString(" "),
"fen" -> chess.format.Forsyth.>>(pov.game.chess),
"status" -> povStatus(pov).toString,
"lastMove" -> pov.game.pgnMoves.lastOption
)
def html(pov: Pov, playing: Boolean)(implicit ctx: Context) = frag(
h1("Textual representation"),
dl(
if (playing) frag(
dt("Your color"),
dd(pov.color.name),
dt("Opponent"),
dd(playerLink(pov.opponent))
)
else frag(
dt("White player"),
dd(playerLink(pov.game.whitePlayer)),
dt("Black player"),
dd(playerLink(pov.game.blackPlayer))
),
dt("PGN"),
dd(
cls := "pgn",
role := "log",
aria.live := "off",
aria.relevant := "additions text"
)(raw(pov.game.pgnMoves.mkString(" "))),
dt("FEN"),
dd(cls := "fen", aria.live := "off")(chess.format.Forsyth.>>(pov.game.chess)),
dt("Game status"),
dd(
cls := "status",
role := "status",
aria.live := "assertive",
aria.atomic := true
)(povStatus(pov)),
dt("Last move"),
dd(
cls := "lastMove",
aria.live := "assertive",
aria.atomic := true
)(pov.game.pgnMoves.lastOption),
(playing && pov.game.playable) option form(
label(
"Your move",
input(name := "move", cls := "move", `type` := "text", cls := "", autocomplete := "off", autofocus := true)
)
),
div(cls := "notify", aria.live := "assertive", aria.atomic := true)
)
)
private def povStatus(pov: Pov)(implicit ctx: Context) =
if (pov.game.finishedOrAborted) gameEndStatus(pov.game)
else "playing"
}

View File

@ -217,10 +217,6 @@ POST /$gameId<\w{8}>/delete controllers.Game.delete(gameId:
GET /round-next/$gameId<\w{8}> controllers.Round.next(gameId: String)
GET /whats-next/$fullId<\w{12}> controllers.Round.whatsNext(fullId: String)
# Round accessibility: text representation
GET /$fullId<\w{12}>/nvui controllers.Round.playerNvui(fullId: String)
GET /$gameId<\w{8}>/$color<white|black>/nvui controllers.Round.watcherNvui(gameId: String, color: String)
# Tournament
GET /tournament controllers.Tournament.home(page: Int ?= 1)
GET /tournament/featured controllers.Tournament.featured

View File

@ -53,8 +53,8 @@ interface Lichess {
dispatchEvent(el: HTMLElement, eventName: string): void;
isTrident: boolean;
isMS: boolean;
NVUI(element: HTMLElement, roundCtrl: any): {
reload(): void;
RoundNVUI(): {
render(ctrl: any): void;
}
}

View File

@ -31,7 +31,7 @@ const plugins = [
{
entries: ['src/plugins/nvui.ts'],
standalone: 'NVUI',
target: 'nvui.min.js'
target: 'round.nvui.min.js'
}
];

View File

@ -1,43 +0,0 @@
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { render as renderGround } from './ground';
import { renderClock } from './clock/clockView';
import { renderInner as tableInner } from './view/table';
import renderCorresClock from './corresClock/corresClockView';
import { Position } from './interfaces';
import RoundController from './ctrl';
let handler: any;
export function view(ctrl: RoundController): VNode {
return h('div.blind', [
h('div.textual', {
hook: {
insert: vnode => init(vnode.elm as HTMLElement, ctrl)
}
}, [ renderGround(ctrl) ]),
h('dt', 'Your clock'),
h('dd.botc', anyClock(ctrl, 'bottom')),
h('dt', 'Opponent clock'),
h('dd.topc', anyClock(ctrl, 'top')),
h('dt', 'Actions'),
h('dd.actions', tableInner(ctrl)),
h('dt', 'Board'),
h('dd.board', h('pre'))
]);
}
function anyClock(ctrl: RoundController, position: Position) {
const d = ctrl.data, player = ctrl.playerAt(position);
return (ctrl.clock && renderClock(ctrl, player, position)) || (
d.correspondence && renderCorresClock(ctrl.corresClock!, ctrl.trans, player.color, position, d.game.player)
) || 'none';
}
function init(el: HTMLElement, ctrl: RoundController) {
if (window.lichess.NVUI) handler = window.lichess.NVUI(el, ctrl);
else window.lichess.loadScript('compiled/nvui.min.js').then(() => init(el, ctrl));
}
export function reload() {
if (handler) handler.reload();
}

View File

@ -5,7 +5,6 @@ import { make as makeSocket, RoundSocket } from './socket';
import * as title from './title';
import * as promotion from './promotion';
import * as blur from './blur';
import * as blind from './blind';
import * as cg from 'chessground/types';
import { Config as CgConfig } from 'chessground/config';
import { Api as CgApi } from 'chessground/api';
@ -22,7 +21,7 @@ import renderUser = require('./view/user');
import cevalSub = require('./cevalSub');
import * as keyboard from './keyboard';
import { RoundOpts, RoundData, ApiMove, ApiEnd, Redraw, SocketMove, SocketDrop, SocketOpts, MoveMetadata, Position } from './interfaces';
import { RoundOpts, RoundData, ApiMove, ApiEnd, Redraw, SocketMove, SocketDrop, SocketOpts, MoveMetadata, Position, Blind } from './interfaces';
interface GoneBerserk {
white?: boolean;
@ -65,6 +64,7 @@ export default class RoundController {
shouldSendMoveTime: boolean = false;
preDrop?: cg.Role;
lastDrawOfferAtPly?: Ply;
blind?: Blind;
private music?: any;
@ -118,6 +118,10 @@ export default class RoundController {
});
if (li.ab && this.isPlaying()) li.ab.init(this);
if (d.blind) window.lichess.loadScript('compiled/round.nvui.min.js').then(() => {
this.blind = window.lichess.RoundNVUI();
this.redraw();
});
}
private showExpiration = () => {
@ -313,7 +317,7 @@ export default class RoundController {
});
};
private playerByColor = (c: Color) =>
playerByColor = (c: Color) =>
this.data[c === this.data.player.color ? 'player' : 'opponent'];
apiMove = (o: ApiMove): void => {
@ -404,7 +408,6 @@ export default class RoundController {
else this.data.expiration.movedAt = Date.now();
}
this.redraw();
if (d.blind) blind.reload();
if (playing && playedColor === d.player.color) {
this.moveOn.next();
cevalSub.publish(d, o);
@ -449,7 +452,6 @@ export default class RoundController {
if (this.corresClock) this.corresClock.update(d.correspondence.white, d.correspondence.black);
if (!this.replaying()) ground.reload(this);
this.setTitle();
if (d.blind) blind.reload();
this.moveOn.next();
this.setQuietMode();
this.redraw();

View File

@ -159,6 +159,9 @@ export interface ApiEnd {
export interface StepCrazy extends Untyped {
}
export interface Blind extends Untyped {
}
export interface Pref {
animationDuration: number;
autoQueen: 1 | 2 | 3;

View File

@ -1,71 +1,122 @@
import { h } from 'snabbdom'
import sanWriter from './sanWriter';
import RoundController from '../ctrl';
import { router } from 'game';
import { throttle } from 'common';
import { renderClock } from '../clock/clockView';
import { renderInner as tableInner } from '../view/table';
import renderCorresClock from '../corresClock/corresClockView';
import { userHtml } from '../view/user';
import { plyStep } from '../round';
import { DecodedDests } from '../interfaces';
import { DecodedDests, Position } from '../interfaces';
import { files } from 'chessground/types';
import { invRanks } from 'chessground/util';
import * as xhr from '../xhr';
import { view as gameView } from 'game';
type Sans = {
[key: string]: Uci;
}
window.lichess.NVUI = function(element: HTMLElement, ctrl: RoundController) {
let $form: JQuery;
const currentFen = () => plyStep(ctrl.data, ctrl.ply).fen
const reload = (first: boolean = false) => {
$.ajax({
url: (ctrl.data.player.spectator ?
router.game(ctrl.data, ctrl.data.player.color) :
router.player(ctrl.data)
) + '/nvui',
headers: first ? {} : xhr.headers,
success(res) {
if (first) {
$(element).html(res);
$form = $(element).find('form').submit(function() {
const input = $form.find('.move').val();
const legalUcis = destsToUcis(ctrl.chessground.state.movable.dests!);
const sans: Sans = sanWriter(currentFen(), legalUcis) as Sans;
const uci = sanToUci(input, sans) || input;
if (legalUcis.indexOf(uci.toLowerCase()) >= 0) ctrl.socket.send("move", {
from: uci.substr(0, 2),
to: uci.substr(2, 2),
promotion: uci.substr(4, 1)
}, { ackable: true });
else {
$(element).find('.notify').text('Invalid move');
}
$form.find('.move').val('');
return false;
});
$form.find('.move').val('').focus();
}
else {
$(element).find('.notify').text('');
['pgn', 'fen', 'status', 'lastMove'].forEach(r => {
if ($(element).find('.' + r).text() != res[r]) {
$(element).find('.' + r).text(res[r]);
}
});
}
$(element).siblings('.board').find('pre').text(textBoard(ctrl));
}
});
}
reload(true);
window.lichess.RoundNVUI = function() {
return {
reload: throttle(1000, reload)
render(ctrl: RoundController) {
const d = ctrl.data,
step = plyStep(d, ctrl.ply);
return h('div.nvui', [
h('h1', 'Textual representation'),
h('dl', [
...(ctrl.isPlaying() ? [
h('dt', 'Your color'),
h('dd', d.player.color),
h('dt', 'Opponent'),
h('dd', userHtml(ctrl, d.player))
] : [
h('dt', 'White player'),
h('dd', userHtml(ctrl, ctrl.playerByColor('white'))),
h('dt', 'Black player'),
h('dd', userHtml(ctrl, ctrl.playerByColor('black')))
]),
h('dt', 'PGN'),
h('dd.pgn', {
attrs: { role : 'log' }
}, d.steps.map(s => s.san).join(' ')),
h('dt', 'FEN'),
// h('dd.fen')(step.fen),
h('dt', 'Game status'),
h('dd.status', {
attrs: {
role : 'status',
'aria.live' : 'assertive',
'aria.atomic' : true
}
}, ctrl.data.game.status.name === 'started' ? 'Playing' : gameView.status(ctrl)),
h('dt', 'Last move'),
h('dd.lastMove', {
attrs: {
'aria.live' : 'assertive',
'aria.atomic' : true
}
}, step.san),
ctrl.isPlaying() ? h('form', {
hook: {
insert(vnode) {
const el = vnode.elm as HTMLFormElement;
const d = ctrl.data;
const $form = $(el).submit(function() {
const input = $form.find('.move').val();
const legalUcis = destsToUcis(ctrl.chessground.state.movable.dests!);
const sans: Sans = sanWriter(plyStep(d, ctrl.ply).fen, legalUcis) as Sans;
const uci = sanToUci(input, sans) || input;
if (legalUcis.indexOf(uci.toLowerCase()) >= 0) ctrl.socket.send("move", {
from: uci.substr(0, 2),
to: uci.substr(2, 2),
promotion: uci.substr(4, 1)
}, { ackable: true });
else {
$(el).siblings('.notify').text('Invalid move');
}
$form.find('.move').val('');
return false;
});
$form.find('.move').val('').focus();
}
}
}, [
h('label', [
'Your move',
h('input.move', {
attrs: {
name : 'move',
'type' : 'text',
autocomplete : 'off'
}
})
])
]) : null,
h('dt', 'Your clock'),
h('dd.botc', anyClock(ctrl, 'bottom')),
h('dt', 'Opponent clock'),
h('dd.topc', anyClock(ctrl, 'top')),
h('dt', 'Actions'),
h('dd.actions', tableInner(ctrl)),
h('dt', 'Board'),
h('dd', h('pre', textBoard(ctrl))),
h('div.notify', {
'aria.live': "assertive",
'aria.atomic' : true
})
])
]);
}
};
}
function anyClock(ctrl: RoundController, position: Position) {
const d = ctrl.data, player = ctrl.playerAt(position);
return (ctrl.clock && renderClock(ctrl, player, position)) || (
d.correspondence && renderCorresClock(ctrl.corresClock!, ctrl.trans, player.color, position, d.game.player)
) || undefined;
}
function destsToUcis(dests: DecodedDests) {
const ucis: string[] = [];
Object.keys(dests).forEach(function(orig) {

View File

@ -6,7 +6,6 @@ import * as promotion from '../promotion';
import { render as renderGround } from '../ground';
import { read as fenRead } from 'chessground/fen';
import * as util from '../util';
import * as blind from '../blind';
import * as keyboard from '../keyboard';
import crazyView from '../crazy/crazyView';
import { render as keyboardMove } from '../keyboardMove';
@ -53,7 +52,7 @@ export function main(ctrl: RoundController): VNode {
material = util.getMaterialDiff(pieces);
score = util.getScore(pieces) * (bottomColor === 'white' ? 1 : -1);
} else material = emptyMaterialDiff;
return d.blind ? blind.view(ctrl) : h('div.round.cg-512', [
return ctrl.blind ? ctrl.blind.render(ctrl) : h('div.round.cg-512', [
h('div.lichess_game.gotomove.variant_' + d.game.variant.key + (ctrl.data.pref.blindfold ? '.blindfold' : ''), {
hook: {
insert: () => window.lichess.pubsub.emit('content_loaded')()