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")
s"$url$fenParam"
}
GameRepo game id flatMap {
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
}
}
Env.explorer.importer(id) map2 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(
common, db, hub, socket, game, round, importer, notifyModule, relation, evalCache
common, db, hub, socket, game, round, importer, notifyModule, relation, evalCache, explorer
)).settings(
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(
play.api,
reactivemongo.driver, reactivemongo.iteratees

View File

@ -6,6 +6,7 @@ import com.typesafe.config.Config
final class Env(
config: Config,
gameColl: lila.db.dsl.Coll,
importer: lila.importer.Importer,
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 {
def receive = {
case lila.game.actorApi.FinishGame(game, _, _) if !game.aborted => indexer(game)
@ -44,6 +36,7 @@ object Env {
lazy val current = "explorer" boot new Env(
config = lila.common.PlayApp loadConfig "explorer",
gameColl = lila.game.Env.current.gameColl,
importer = lila.importer.Env.current.importer,
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)
}
case ("explorerGame", o) =>
reading[actorApi.ExplorerGame](o) { data =>
member.userId foreach { byUserId =>
api.explorerGame(byUserId, studyId, data, uid)
}
}
case ("like", o) => for {
byUserId <- member.userId
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 setTagReader = Json.reads[actorApi.SetTag]
private implicit val gamebookReader = Json.reads[Gamebook]
private implicit val explorerGame = Json.reads[actorApi.ExplorerGame]
def join(
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) {
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;
padding: 3px 5px;
border-radius: 3px;
font-family: 'Roboto Mono', 'Roboto';
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 {
width: 100%;
}

View File

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

View File

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

View File

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

View File

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

View File

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