study explorer game insertion - WIP

study-explorer-insert
Thibault Duplessis 2017-09-18 21:03:23 -05:00
parent 0b8340fa1f
commit 7866dd59ab
14 changed files with 193 additions and 57 deletions

View File

@ -54,16 +54,6 @@ object Importer extends LilaController {
val fenParam = get("fen").??(f => s"?fen=$f") val fenParam = get("fen").??(f => s"?fen=$f")
s"$url$fenParam" s"$url$fenParam"
} }
GameRepo game id flatMap { Env.explorer.importer(id) map2 redirectAtFen
case Some(game) if game.createdAt.isAfter(masterGameEncodingFixedAt) => fuccess(redirectAtFen(game))
case _ => (GameRepo remove id) >> Env.explorer.fetchPgn(id) flatMap {
case None => fuccess(NotFound)
case Some(pgn) => env.importer(
lila.importer.ImportData(pgn, none),
user = "lichess".some,
forceId = id.some
) map redirectAtFen
}
}
} }
} }

View File

@ -272,7 +272,7 @@ lazy val challenge = module("challenge", Seq(common, db, hub, setup, game, relat
) )
lazy val study = module("study", Seq( lazy val study = module("study", Seq(
common, db, hub, socket, game, round, importer, notifyModule, relation, evalCache common, db, hub, socket, game, round, importer, notifyModule, relation, evalCache, explorer
)).settings( )).settings(
libraryDependencies ++= provided(play.api, reactivemongo.driver) libraryDependencies ++= provided(play.api, reactivemongo.driver)
) )
@ -370,7 +370,7 @@ lazy val report = module("report", Seq(common, db, user, game, security)).settin
) )
) )
lazy val explorer = module("explorer", Seq(common, db, game)).settings( lazy val explorer = module("explorer", Seq(common, db, game, importer)).settings(
libraryDependencies ++= provided( libraryDependencies ++= provided(
play.api, play.api,
reactivemongo.driver, reactivemongo.iteratees reactivemongo.driver, reactivemongo.iteratees

View File

@ -6,6 +6,7 @@ import com.typesafe.config.Config
final class Env( final class Env(
config: Config, config: Config,
gameColl: lila.db.dsl.Coll, gameColl: lila.db.dsl.Coll,
importer: lila.importer.Importer,
system: ActorSystem system: ActorSystem
) { ) {
@ -23,15 +24,6 @@ final class Env(
} }
} }
def fetchPgn(id: String): Fu[Option[String]] = {
import play.api.libs.ws.WS
import play.api.Play.current
WS.url(s"$InternalEndpoint/master/pgn/$id").get() map {
case res if res.status == 200 => res.body.some
case _ => None
}
}
if (IndexFlow) system.lilaBus.subscribe(system.actorOf(Props(new Actor { if (IndexFlow) system.lilaBus.subscribe(system.actorOf(Props(new Actor {
def receive = { def receive = {
case lila.game.actorApi.FinishGame(game, _, _) if !game.aborted => indexer(game) case lila.game.actorApi.FinishGame(game, _, _) if !game.aborted => indexer(game)
@ -44,6 +36,7 @@ object Env {
lazy val current = "explorer" boot new Env( lazy val current = "explorer" boot new Env(
config = lila.common.PlayApp loadConfig "explorer", config = lila.common.PlayApp loadConfig "explorer",
gameColl = lila.game.Env.current.gameColl, gameColl = lila.game.Env.current.gameColl,
importer = lila.importer.Env.current.importer,
system = lila.common.PlayApp.system system = lila.common.PlayApp.system
) )
} }

View File

@ -0,0 +1,36 @@
package lila.explorer
import org.joda.time.DateTime
import lila.game.{ Game, GameRepo }
import lila.importer.{ Importer, ImportData }
final class ExplorerImport(
endpoint: String,
importer: Importer
) {
private val masterGameEncodingFixedAt = new DateTime(2016, 3, 9, 0, 0)
def apply(id: Game.ID): Fu[Option[Game]] =
GameRepo game id flatMap {
case Some(game) if game.createdAt.isAfter(masterGameEncodingFixedAt) => fuccess(game.some)
case _ => (GameRepo remove id) >> fetchPgn(id) flatMap {
case None => fuccess(none)
case Some(pgn) => importer(
ImportData(pgn, none),
user = "lichess".some,
forceId = id.some
) map some
}
}
private def fetchPgn(id: String): Fu[Option[String]] = {
import play.api.libs.ws.WS
import play.api.Play.current
WS.url(s"$endpoint/master/pgn/$id").get() map {
case res if res.status == 200 => res.body.some
case _ => None
}
}
}

View File

@ -0,0 +1,38 @@
package lila.study
import lila.game.{ Game, Namer, PgnImport }
import lila.user.User
import chess.format.pgn.{ Parser, Reader, ParsedPgn, Tag, TagType }
private final class ExplorerGame(
importer: lila.explorer.ExplorerImport
) {
def quote(userId: User.ID, study: Study, chapter: Chapter, path: Path, gameId: Game.ID): Fu[Comment] =
importer(gameId) flatMap {
_.fold(false) { game =>
val comment = Comment(
id = Comment.Id.make,
text = game.pgnImport.fold(lichessTitle)(importTitle(game)),
by = Comment.Author.User(author.id, author.titleName)
)
}
}
private def importTitle(g: Game)(pgnImport: PgnImport): String =
Parser.full(pgnImport.pgn) flatMap {
case ParsedPgn(_, tags, _) =>
def tag(which: Tag.type => TagType): Option[String] =
tags find (_.name == which(Tag)) map (_.value)
ImportData(pgnImport.pgn, none).preprocess(none).fold(
_ => lichessTitle(g),
processed =>
val players = Namer.vsText(game, withRatings = true)
val result = chess.Color.showResult(game.winnerColor)
val text = s"$players, $result
}
def insert(userId: User.ID, study: Study, chapter: Chapter, gameId: Game.ID) = ???
}

View File

@ -228,6 +228,13 @@ private[study] final class SocketHandler(
} api.toggleGlyph(userId, studyId, position.ref, glyph, uid) } api.toggleGlyph(userId, studyId, position.ref, glyph, uid)
} }
case ("explorerGame", o) =>
reading[actorApi.ExplorerGame](o) { data =>
member.userId foreach { byUserId =>
api.explorerGame(byUserId, studyId, data, uid)
}
}
case ("like", o) => for { case ("like", o) => for {
byUserId <- member.userId byUserId <- member.userId
v <- (o \ "d" \ "liked").asOpt[Boolean] v <- (o \ "d" \ "liked").asOpt[Boolean]
@ -260,6 +267,7 @@ private[study] final class SocketHandler(
private implicit val StudyDataReader = Json.reads[Study.Data] private implicit val StudyDataReader = Json.reads[Study.Data]
private implicit val setTagReader = Json.reads[actorApi.SetTag] private implicit val setTagReader = Json.reads[actorApi.SetTag]
private implicit val gamebookReader = Json.reads[Gamebook] private implicit val gamebookReader = Json.reads[Gamebook]
private implicit val explorerGame = Json.reads[actorApi.ExplorerGame]
def join( def join(
studyId: Study.Id, studyId: Study.Id,

View File

@ -0,0 +1,19 @@
package lila.study
private final class StudyCommenter() {
def apply(chapter: Chapteer, position: Position, comment: Comment) =
chapter.setComment(comment, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(study)
newChapter.root.nodeAt(position.path) ?? { node =>
node.comments.findBy(comment.by) ?? { c =>
chapterRepo.setComments(newChapter, position.path, node.comments.filterEmpty) >>- {
sendTo(study, Socket.SetComment(position, c, uid))
indexStudy(study)
sendStudyEnters(study, userId)
}
}
}
}
}

View File

@ -6,3 +6,4 @@ case class SaveStudy(study: Study)
case class SetTag(chapterId: Chapter.Id, name: String, value: String) { case class SetTag(chapterId: Chapter.Id, name: String, value: String) {
def tag = chess.format.pgn.Tag(name, value take 140) def tag = chess.format.pgn.Tag(name, value take 140)
} }
case class ExplorerGame(chapterId: Chapter.Id, path: String, gameId: String, insert: Boolean)

View File

@ -608,9 +608,30 @@ body.dark .explorer_box .black {
text-align: center; text-align: center;
padding: 3px 5px; padding: 3px 5px;
border-radius: 3px; border-radius: 3px;
font-family: 'Roboto Mono', 'Roboto';
font-size: 0.9em; font-size: 0.9em;
} }
.explorer_box td.game_menu {
background: #759900;
cursor: default;
padding: 5px 0 0 0;
}
.explorer_box .game_menu .game_title {
text-align: center;
font-style: italic;
color: #fff;
}
.explorer_box .game_menu .menu {
display: flex;
justify-content: space-between;
text-transform: uppercase;
}
.explorer_box .game_menu .menu a {
color: #fff;
padding: 5px;
}
.explorer_box .game_menu .menu a:hover {
background: rgba(255,255,255,0.2);
}
.explorer_box .tablebase { .explorer_box .tablebase {
width: 100%; width: 100%;
} }

View File

@ -17,13 +17,15 @@ function tablebaseRelevant(variant: string, fen: Fen) {
} }
export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl { export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl {
const allowed = prop(allow); const allowed = prop(allow),
const enabled = root.embed ? prop(false) : storedProp('explorer.enabled', false); enabled = root.embed ? prop(false) : storedProp('explorer.enabled', false),
loading = prop(true),
failing = prop(false),
hovering = prop<Hovering | null>(null),
movesAway = prop(0),
gameMenu = prop<string | null>(null);
if ((location.hash === '#explorer' || location.hash === '#opening') && !root.embed) enabled(true); if ((location.hash === '#explorer' || location.hash === '#opening') && !root.embed) enabled(true);
const loading = prop(true);
const failing = prop(false);
const hovering = prop<Hovering | null>(null);
const movesAway = prop(0);
let cache = {}; let cache = {};
function onConfigClose() { function onConfigClose() {
@ -62,6 +64,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl {
function setNode() { function setNode() {
if (!enabled()) return; if (!enabled()) return;
gameMenu(null);
const node = root.node; const node = root.node;
if (node.ply > 50 && !tablebaseRelevant(effectiveVariant, node.fen)) { if (node.ply > 50 && !tablebaseRelevant(effectiveVariant, node.fen)) {
cache[node.fen] = empty; cache[node.fen] = empty;
@ -87,6 +90,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl {
movesAway, movesAway,
config, config,
withGames, withGames,
gameMenu,
current: () => cache[root.node.fen], current: () => cache[root.node.fen],
toggle() { toggle() {
movesAway(0); movesAway(0);
@ -97,6 +101,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl {
disable() { disable() {
if (enabled()) { if (enabled()) {
enabled(false); enabled(false);
gameMenu(null);
root.autoScroll(); root.autoScroll();
} }
}, },

View File

@ -79,6 +79,7 @@ function showResult(winner: Color): VNode {
function showGameTable(ctrl: AnalyseCtrl, title: string, games): VNode | null { function showGameTable(ctrl: AnalyseCtrl, title: string, games): VNode | null {
if (!ctrl.explorer.withGames || !games.length) return null; if (!ctrl.explorer.withGames || !games.length) return null;
const openedId = ctrl.explorer.gameMenu();
return h('table.games', [ return h('table.games', [
h('thead', [ h('thead', [
h('tr', [ h('tr', [
@ -86,29 +87,46 @@ function showGameTable(ctrl: AnalyseCtrl, title: string, games): VNode | null {
]) ])
]), ]),
h('tbody', { h('tbody', {
insert: vnode => { hook: bind('click', e => {
const el = vnode.elm as HTMLElement; const $tr = $(e.target).parents('tr');
el.addEventListener('click', e => { if (!$tr.length) return;
const $tr = $(e.target).parents('tr'); ctrl.explorer.gameMenu($tr.data('id'));
if (!$tr.length) return; ctrl.redraw();
const orientation = ctrl.chessground.state.orientation; })
const fenParam = ctrl.node.ply > 0 ? ('?fen=' + ctrl.node.fen) : ''; }, games.map(game => {
if (ctrl.explorer.config.data.db.selected() === 'lichess') return openedId && openedId === game.id ? h('tr', [
window.open('/' + $tr.data('id') + '/' + orientation + fenParam, '_blank'); h('td.game_menu', {
else window.open('/import/master/' + $tr.data('id') + '/' + orientation + fenParam, '_blank'); attrs: { colspan: 4 },
}); }, [
// el.oncontextmenu = (e: MouseEvent) => { h('div.game_title', `${game.white.name} - ${game.black.name}, ${showResult(game.winner).text}, ${game.year}`),
// const path = eventPath(e); h('div.menu', [
// if (path !== null) contextMenu(e, { h('a.text', {
// path, attrs: dataIcon('v'),
// root: ctrl hook: bind('click', _ => {
// }); const orientation = ctrl.chessground.state.orientation,
// ctrl.redraw(); fenParam = ctrl.node.ply > 0 ? ('?fen=' + ctrl.node.fen) : '';
// return false; if (ctrl.explorer.config.data.db.selected() === 'lichess')
// }; window.open('/' + openedId + '/' + orientation + fenParam, '_blank');
} else window.open('/import/master/' + openedId + '/' + orientation + fenParam, '_blank');
}, games.map(function(game) { })
return h('tr', { }, 'View'),
...(ctrl.study ? [
h('a.text', {
attrs: dataIcon('c'),
hook: bind('click', _ => ctrl.study!.explorerGame(openedId, false), ctrl.redraw)
}, 'Quote'),
h('a.text', {
attrs: dataIcon('O'),
hook: bind('click', _ => ctrl.study!.explorerGame(openedId, true), ctrl.redraw)
}, 'Insert')
] : []),
h('a.text', {
attrs: dataIcon('L'),
hook: bind('click', _ => ctrl.explorer.gameMenu(null), ctrl.redraw)
}, 'Close')
])
])
]) : h('tr', {
key: game.id, key: game.id,
attrs: { 'data-id': game.id } attrs: { 'data-id': game.id }
}, [ }, [

View File

@ -93,6 +93,7 @@ export interface ExplorerCtrl {
movesAway: Prop<number>; movesAway: Prop<number>;
config: ExplorerConfigCtrl; config: ExplorerConfigCtrl;
withGames: boolean; withGames: boolean;
gameMenu: Prop<string | null>;
current(): ExplorerData | undefined; current(): ExplorerData | undefined;
hovering: Prop<Hovering | null>; hovering: Prop<Hovering | null>;
setNode(); setNode();

View File

@ -42,6 +42,7 @@ export interface StudyCtrl {
mutateCgConfig(config: any): void; mutateCgConfig(config: any): void;
isUpdatedRecently(): boolean; isUpdatedRecently(): boolean;
setGamebookOverride(o: GamebookOverride): void; setGamebookOverride(o: GamebookOverride): void;
explorerGame(gameId: string, insert: boolean): void;
redraw(): void; redraw(): void;
} }

View File

@ -247,6 +247,12 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
vm.updatedAt = Date.now(); vm.updatedAt = Date.now();
} }
function withPosition(obj: any) {
obj.ch = vm.chapterId;
obj.path = ctrl.path;
return obj;
}
return { return {
data, data,
form, form,
@ -284,11 +290,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
if (practice) practice.onJump(); if (practice) practice.onJump();
if (gamebookPlay) gamebookPlay.onJump(); if (gamebookPlay) gamebookPlay.onJump();
}, },
withPosition(obj) { withPosition,
obj.ch = vm.chapterId;
obj.path = ctrl.path;
return obj;
},
setPath(path, node) { setPath(path, node) {
onSetPath(path); onSetPath(path);
setTimeout(() => commentForm.onSetPath(path, node), 100); setTimeout(() => commentForm.onSetPath(path, node), 100);
@ -347,6 +349,9 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
if (!o) xhrReload(); if (!o) xhrReload();
}, },
mutateCgConfig, mutateCgConfig,
explorerGame(gameId: string, insert: boolean) {
makeChange('explorerGame', withPosition({ gameId, insert }));
},
redraw, redraw,
socketHandlers: { socketHandlers: {
path(d) { path(d) {