From 1ef65040168ab7d55ce921abc9d63644a937d689 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Mon, 30 Mar 2020 21:05:46 +0200 Subject: [PATCH] Experimental game upload added --- client/src/components/BaseGame.vue | 36 ++++--- client/src/components/Board.vue | 16 ++-- client/src/components/MoveList.vue | 6 +- client/src/components/UploadGame.vue | 90 +++++++++++++++++- client/src/main.js | 5 +- client/src/translations/en.js | 3 +- client/src/translations/es.js | 3 +- client/src/translations/fr.js | 3 +- client/src/utils/gameStorage.js | 1 + client/src/utils/importgameStorage.js | 2 +- client/src/utils/modalClick.js | 5 +- client/src/views/Game.vue | 130 +++++++++++++++----------- client/src/views/MyGames.vue | 27 ++++-- server/db/create.sql | 2 + server/models/Game.js | 9 ++ 15 files changed, 238 insertions(+), 100 deletions(-) diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index c3387e82..fd26d872 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -111,7 +111,7 @@ export default { : "" ); }, - // TODO: is it OK to pass "computed" as propoerties? + // TODO: is it OK to pass "computed" as properties? // Also, some are seemingly not recomputed when vr is initialized. showMoves: function() { return this.game.score != "*" @@ -211,6 +211,7 @@ export default { // Strategy working also for multi-moves: if (!Array.isArray(move)) move = [move]; move.forEach((m,idx) => { + m.index = this.vr.movesCount; m.notation = this.vr.getNotation(m); m.unambiguous = V.GetUnambiguousNotation(m); this.vr.play(m); @@ -221,6 +222,7 @@ export default { if (firstMoveColor == "b") { // 'start' & 'end' is required for Board component this.moves.unshift({ + index: parsedFen.movesCount, notation: "...", unambiguous: "...", start: { x: -1, y: -1 }, @@ -271,30 +273,36 @@ export default { let pgn = ""; pgn += '[Site "vchess.club"]\n'; pgn += '[Variant "' + this.game.vname + '"]\n'; - pgn += '[Date "' + getDate(new Date()) + '"]\n'; + const gdt = getDate(new Date(this.game.created || Date.now())); + pgn += '[Date "' + gdt + '"]\n'; pgn += '[White "' + this.game.players[0].name + '"]\n'; pgn += '[Black "' + this.game.players[1].name + '"]\n'; pgn += '[Fen "' + this.game.fenStart + '"]\n'; pgn += '[Result "' + this.game.score + '"]\n'; - if (!!this.game.id) - pgn += '[URL "' + params.serverUrl + '/game/' + this.game.id + '"]\n'; + if (!!this.game.id) { + pgn += '[Cadence "' + this.game.cadence + '"]\n'; + pgn += '[Url "' + params.serverUrl + '/game/' + this.game.id + '"]\n'; + } pgn += '\n'; for (let i = 0; i < this.moves.length; i += 2) { if (i > 0) pgn += " "; // Adjust dots notation for a better display: let fullNotation = getFullNotation(this.moves[i]); if (fullNotation == "...") fullNotation = ".."; - pgn += (i/2+1) + "." + fullNotation; + pgn += (this.moves[i].index / 2 + 1) + "." + fullNotation; if (i+1 < this.moves.length) pgn += " " + getFullNotation(this.moves[i+1]); } pgn += "\n\n"; for (let i = 0; i < this.moves.length; i += 2) { - const moveNumber = i / 2 + 1; - pgn += moveNumber + "." + i + " " + - getFullNotation(this.moves[i], "unambiguous") + "\n"; + const moveNumber = this.moves[i].index / 2 + 1; + // Skip "dots move", useless for machine reading: + if (this.moves[i].notation != "...") { + pgn += moveNumber + ".w " + + getFullNotation(this.moves[i], "unambiguous") + "\n"; + } if (i+1 < this.moves.length) { - pgn += moveNumber + "." + (i+1) + " " + + pgn += moveNumber + ".b " + getFullNotation(this.moves[i+1], "unambiguous") + "\n"; } } @@ -323,8 +331,14 @@ export default { this.autoplayLoop = null; } else { this.autoplay = true; - infinitePlay(); - this.autoplayLoop = setInterval(infinitePlay, 1500); + setTimeout( + () => { + infinitePlay(); + this.autoplayLoop = setInterval(infinitePlay, 1500); + }, + // Small delay otherwise the first move is played too fast + 500 + ); } }, // Animate an elementary move diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index 639938b2..56f34880 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -519,12 +519,11 @@ export default { }; }, mousedown: function(e) { - if (!([1, 3].includes(e.which))) return; e.preventDefault(); - if (e.which != 3) + if (!this.mobileBrowser && e.which != 3) // Cancel current drawing and circles, if any this.cancelResetArrows(); - if (e.which == 1 || this.mobileBrowser) { + if (this.mobileBrowser || e.which == 1) { // Mouse left button if (!this.start) { // NOTE: classList[0] is enough: 'piece' is the first assigned class @@ -566,8 +565,8 @@ export default { } else { this.processMoveAttempt(e); } - } else { - // e.which == 3 : mouse right button + } else if (e.which == 3) { + // Mouse right button let elem = e.target; // Next loop because of potential marks while (elem.tagName == "IMG") elem = elem.parentNode; @@ -612,17 +611,16 @@ export default { } }, mouseup: function(e) { - if (!([1, 3].includes(e.which))) return; e.preventDefault(); - if (e.which == 1) { + if (this.mobileBrowser || e.which == 1) { if (!this.selectedPiece) return; // Drag'n drop. Selected piece is no longer needed: this.selectedPiece.parentNode.removeChild(this.selectedPiece); delete this.selectedPiece; this.selectedPiece = null; this.processMoveAttempt(e); - } else { - // Mouse right button (e.which == 3) + } else if (e.which == 3) { + // Mouse right button this.movingArrow = { x: -1, y: -1 }; this.processArrowAttempt(e); } diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue index d2ae3f35..4c351dd5 100644 --- a/client/src/components/MoveList.vue +++ b/client/src/components/MoveList.vue @@ -101,10 +101,8 @@ export default { window.addEventListener("resize", () => { if (!timeoutLaunched) { timeoutLaunched = true; - setTimeout(() => { - this.adjustBoard(); - timeoutLaunched = false; - }, 500); + this.adjustBoard(); + setTimeout(() => { timeoutLaunched = false; }, 500); } }); }, diff --git a/client/src/components/UploadGame.vue b/client/src/components/UploadGame.vue index 369333c1..a94c39b7 100644 --- a/client/src/components/UploadGame.vue +++ b/client/src/components/UploadGame.vue @@ -9,6 +9,7 @@ div </template> <script> +import { getRandString } from "@/utils/alea"; export default { name: "my-upload-game", methods: { @@ -23,11 +24,90 @@ export default { }; reader.readAsText(file); }, - parseAndEmit: function(pgn) { - // TODO: header gives game Info, third secton the moves - let game = {}; - // mark sur ID pour dire import : I_ - this.$emit("game-uploaded", game); + parseAndEmit: async function(pgn) { + let game = { + // Players potential ID and socket IDs are not searched + players: [ + { id: 0, sid: "" }, + { id: 0, sid: "" } + ] + }; + const lines = pgn.split('\n'); + let idx = 0; + // Read header + while (lines[idx].length > 0) { + // NOTE: not using "split(' ')" because the FEN has spaces + const spaceIdx = lines[idx].indexOf(' '); + const prop = lines[idx].substr(0, spaceIdx).match(/^\[(.*)$/)[1]; + const value = lines[idx].substr(spaceIdx + 1).match(/^"(.*)"\]$/)[1]; + switch (prop) { + case "Variant": + game.vname = value; + break; + case "Date": + game.created = new Date(value).getTime(); + break; + case "White": + game.players[0].name = value; + break; + case "Black": + game.players[1].name = value; + break; + case "Fen": + game.fenStart = value; + break; + case "Result": + // Allow importing unfinished games, but mark them as + // "unknown result" to avoid running the clocks... + game.result = (value != "*" ? value : "?"); + break; + case "Url": + // Prefix "I_" to say "this is an import" + game.id = "i" + value.match(/\/game\/([a-zA-Z0-9]+)$/)[1]; + break; + case "Cadence": + game.cadence = value; + break; + } + idx++; + } + if (!game.id) { + game.id = "i" + getRandString(); + // Provide a random cadence, just to be sure nothing breaks: + game.cadence = "1d"; + } + game.chats = []; //not stored in PGN :) + // Skip "human moves" section: + while (lines[++idx].length > 0) {} + // Read moves + game.moves = []; + await import("@/variants/" + game.vname + ".js") + .then((vModule) => { + window.V = vModule[game.vname + "Rules"]; + while (++idx < lines.length && lines[idx].length > 0) { + const lineParts = lines[idx].split(" "); + const startEnd = lineParts[1].split('.'); + let move = {}; + if (startEnd[0] != "-") move.start = V.SquareToCoords(startEnd[0]); + if (startEnd[1] != "-") move.end = V.SquareToCoords(startEnd[1]); + const appearVanish = lineParts[2].split('/').map(lpart => { + if (lpart == "-") return []; + return lpart.split('.').map(psq => { + const xy = V.SquareToCoords(psq.substr(2)); + return { + x: xy.x, + y: xy.y, + c: psq[0], + p: psq[1] + }; + }); + }); + move.appear = appearVanish[0]; + move.vanish = appearVanish[1]; + game.moves.push(move); + } + this.$emit("game-uploaded", game); + }); } } }; diff --git a/client/src/main.js b/client/src/main.js index f2e38f1f..3577b155 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -15,14 +15,15 @@ new Vue({ window.doClick = elemId => { document.getElementById(elemId).click(); }; - // Esc key can close modals: + // Esc key can close some modals: document.addEventListener("keydown", e => { if (e.code === "Escape") { let modalBoxes = document.querySelectorAll("[id^='modal']"); modalBoxes.forEach(m => { if ( m.checked && - !["modalAccept","modalConfirm"].includes(m.id) + !["modalAccept", "modalConfirm", "modalChat", "modalPeople"] + .includes(m.id) ) { m.checked = false; } diff --git a/client/src/translations/en.js b/client/src/translations/en.js index e1d6189e..e8c1e5c1 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -4,6 +4,7 @@ export const translations = { About: "About", "Accept draw?": "Accept draw?", "Accept challenge?": "Accept challenge?", + "An error occurred. Try again!": "An error occurred. Try again!", Analyse: "Analyse", "Analysis mode": "Analysis mode", "Analysis disabled for this variant": "Analysis disabled for this variant", @@ -86,7 +87,6 @@ export const translations = { News: "News", "No challenges found :( Click on 'New game'!": "No challenges found :( Click on 'New game'!", "No games found :( Send a challenge!": "No games found :( Send a challenge!", - "No identifier found: use the upload button in analysis mode": "No identifier found: use the upload button in analysis mode", "No more problems": "No more problems", "No subject. Send anyway?": "No subject. Send anyway?", "Notifications by email": "Notifications by email", @@ -135,6 +135,7 @@ export const translations = { "Symmetric random": "Symmetric random", "Terminate game?": "Terminate game?", "The game should be in another tab": "The game should be in another tab", + "The game was already imported": "The game was already imported", "Three repetitions": "Three repetitions", Time: "Time", "Undetermined result": "Undetermined result", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index d14f4606..85b458da 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -4,6 +4,7 @@ export const translations = { About: "Acerca de", "Accept draw?": "¿Acceptar tablas?", "Accept challenge?": "¿Acceptar el desafÃo?", + "An error occurred. Try again!": "Se ha producido un error. ¡Intenta de nuevo!", Analyse: "Analizar", "Analysis mode": "Modo análisis", "Analysis disabled for this variant": "Análisis deshabilitado para esta variante", @@ -86,7 +87,6 @@ export const translations = { News: "Noticias", "No challenges found :( Click on 'New game'!": "No se encontró ningún desafÃo :( ¡Haz clic en 'Nueva partida'!", "No games found :( Send a challenge!": "No se encontró partidas :( ¡EnvÃa un desafÃo!", - "No identifier found: use the upload button in analysis mode": "No se encontró ningún identificador: use el botón enviar en modo de análisis", "No more problems": "No mas problemas", "No subject. Send anyway?": "Sin asunto. ¿Enviar sin embargo?", "Notifications by email": "Notificaciones por email", @@ -135,6 +135,7 @@ export const translations = { "Symmetric random": "Aleatorio simétrico", "Terminate game?": "¿Terminar la partida?", "The game should be in another tab": "la partida deberÃa estar en otra pestaña", + "The game was already imported": "La partida ya ha sido importada", "Three repetitions": "Tres repeticiones", Time: "Tiempo", "Undetermined result": "Resultado indeterminado", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 2d7b535b..493b5b5f 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -4,6 +4,7 @@ export const translations = { About: "à propos", "Accept draw?": "Accepter la nulle ?", "Accept challenge?": "Accepter le défi ?", + "An error occurred. Try again!": "Une erreur est survenue. Réessayez !", Analyse: "Analyser", "Analysis mode": "Mode analyse", "Analysis disabled for this variant": "Analyse désactivée pour cette variante", @@ -86,7 +87,6 @@ export const translations = { News: "Nouvelles", "No challenges found :( Click on 'New game'!": "Aucun défi trouvé :( Cliquez sur 'Nouvelle partie' !", "No games found :( Send a challenge!": "Aucune partie trouvée :( Envoyez un défi !", - "No identifier found: use the upload button in analysis mode": "Pas d'identifiant trouvé : utilisez le bouton d'envoi en mode analyse", "No more problems": "Plus de problèmes", "No subject. Send anyway?": "Pas de sujet. Envoyer quand-même ??", "Notifications by email": "Notifications par email", @@ -135,6 +135,7 @@ export const translations = { "Symmetric random": "Aléatoire symétrique", "Terminate game?": "Stopper la partie ?", "The game should be in another tab": "La partie devrait être dans un autre onglet", + "The game was already imported": "La partie a déjà été importée", "Three repetitions": "Triple répétition", Time: "Temps", "Undetermined result": "Résultat indéterminé", diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index 109c8251..f69de2e2 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -87,6 +87,7 @@ export const GameStorage = { Object.keys(obj).forEach(k => { if (k == "move") game.moves.push(obj[k]); else if (k == "chat") game.chats.push(obj[k]); + else if (k == "chatRead") game.chatRead = Date.now(); else if (k == "delchat") game.chats = []; else game[k] = obj[k]; }); diff --git a/client/src/utils/importgameStorage.js b/client/src/utils/importgameStorage.js index e5ad6e22..46a60bbb 100644 --- a/client/src/utils/importgameStorage.js +++ b/client/src/utils/importgameStorage.js @@ -47,7 +47,7 @@ export const ImportgameStorage = { }; transaction.onerror = function(err) { // Duplicate key error (most likely) - callback(err); + callback(err.target.error); }; transaction.objectStore("importgames").add(game); }); diff --git a/client/src/utils/modalClick.js b/client/src/utils/modalClick.js index 96935787..f4687fde 100644 --- a/client/src/utils/modalClick.js +++ b/client/src/utils/modalClick.js @@ -1,5 +1,6 @@ -export function processModalClick(e) { +export function processModalClick(e, cb) { // Close a modal when click on it but outside focused element const data = e.target.dataset; - if (data.checkbox) document.getElementById(data.checkbox).checked = false; + if (!!data.checkbox) document.getElementById(data.checkbox).checked = false; + if (!!cb) cb(); } diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 2a565f68..5145e588 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -57,7 +57,7 @@ main span {{ st.tr["Cancel"] }} .row #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2 - span.variant-cadence {{ game.cadence }} + span.variant-cadence(v-if="game.type!='import'") {{ game.cadence }} span.variant-name {{ game.vname }} span#nextGame( v-if="nextIds.length > 0" @@ -236,10 +236,14 @@ export default { this.atCreation(); }, mounted: function() { - ["chatWrap", "infoDiv"].forEach(eltName => { - document.getElementById(eltName) - .addEventListener("click", processModalClick); - }); + document.getElementById("chatWrap") + .addEventListener("click", (e) => { + processModalClick(e, () => { + this.toggleChat("close") + }); + }); + document.getElementById("infoDiv") + .addEventListener("click", processModalClick); if ("ontouchstart" in window) { // Disable tooltips on smartphones: document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => { @@ -441,22 +445,33 @@ export default { if (!!oppsid && !!this.people[oppsid]) return oppsid; return null; }, - toggleChat: function() { - if (document.getElementById("modalChat").checked) + // NOTE: action if provided is always a closing action + toggleChat: function(action) { + if (!action && document.getElementById("modalChat").checked) // Entering chat document.getElementById("inputChat").focus(); - // TODO: next line is only required when exiting chat, - // but the event for now isn't well detected. - document.getElementById("chatBtn").classList.remove("somethingnew"); + else { + document.getElementById("chatBtn").classList.remove("somethingnew"); + if (!!this.game.mycolor) { + // Update "chatRead" variable either on server or locally + if (this.game.type == "corr") + this.updateCorrGame({ chatRead: this.game.mycolor }); + else if (this.game.type == "live") + GameStorage.update(this.gameRef, { chatRead: true }); + } + } }, processChat: function(chat) { this.send("newchat", { data: chat }); // NOTE: anonymous chats in corr games are not stored on server (TODO?) - if (this.game.type == "corr" && this.st.user.id > 0) - this.updateCorrGame({ chat: chat }); - else if (this.game.type == "live") { - chat.added = Date.now(); - GameStorage.update(this.gameRef, { chat: chat }); + if (!!this.game.mycolor) { + if (this.game.type == "corr") + this.updateCorrGame({ chat: chat }); + else { + // Live game + chat.added = Date.now(); + GameStorage.update(this.gameRef, { chat: chat }); + } } }, clearChat: function() { @@ -475,6 +490,7 @@ export default { } }, getGameType: function(game) { + if (!!game.id.match(/^i/)) return "import"; return game.cadence.indexOf("d") >= 0 ? "corr" : "live"; }, // Notify something after a new move (to opponent and me on MyGames page) @@ -640,13 +656,15 @@ export default { break; } case "askgame": - // Send current (live) game if not asked by any of the players + // Send current (live or import) game, + // if not asked by any of the players if ( - this.game.type == "live" && + this.game.type != "corr" && this.game.players.every(p => p.sid != data.from[0]) ) { const myGame = { id: this.game.id, + // FEN is current position, unused for now fen: this.game.fen, players: this.game.players, vid: this.game.vid, @@ -784,7 +802,7 @@ export default { case "drawoffer": // NOTE: observers don't know who offered draw this.drawOffer = "received"; - if (this.game.type == "live") { + if (!!this.game.mycolor && this.game.type == "live") { GameStorage.update( this.gameRef, { drawOffer: V.GetOppCol(this.game.mycolor) } @@ -794,7 +812,7 @@ export default { case "rematchoffer": // NOTE: observers don't know who offered rematch this.rematchOffer = data.data ? "received" : ""; - if (this.game.type == "live") { + if (!!this.game.mycolor && this.game.type == "live") { GameStorage.update( this.gameRef, { rematchOffer: V.GetOppCol(this.game.mycolor) } @@ -826,7 +844,8 @@ export default { this.$refs["chatcomp"].newChat(chat); if (this.game.type == "live") { chat.added = Date.now(); - GameStorage.update(this.gameRef, { chat: chat }); + if (!!this.game.mycolor) + GameStorage.update(this.gameRef, { chat: chat }); } if (!document.getElementById("modalChat").checked) document.getElementById("chatBtn").classList.add("somethingnew"); @@ -893,7 +912,7 @@ export default { } }, clickDraw: function() { - if (!this.game.mycolor) return; //I'm just spectator + if (!this.game.mycolor || this.game.type == "import") return; if (["received", "threerep"].includes(this.drawOffer)) { if (!confirm(this.st.tr["Accept draw?"])) return; const message = @@ -945,7 +964,7 @@ export default { }); }, clickRematch: function() { - if (!this.game.mycolor) return; //I'm just spectator + if (!this.game.mycolor || this.game.type == "import") return; if (this.rematchOffer == "received") { // Start a new game! let gameInfo = { @@ -1018,19 +1037,16 @@ export default { this.gameOver(score, side + " surrender"); }, loadGame: function(game, callback) { - this.vr = new V(game.fen); - const gtype = this.getGameType(game); + const gtype = game.type || this.getGameType(game); const tc = extractTime(game.cadence); const myIdx = game.players.findIndex(p => { return p.sid == this.st.user.sid || p.id == this.st.user.id; }); // "mycolor" is undefined for observers const mycolor = [undefined, "w", "b"][myIdx + 1]; - // Live games before 26/03/2020 don't have chat history: - if (!game.chats) game.chats = []; //TODO: remove line - // Sort chat messages from newest to oldest - game.chats.sort((c1, c2) => c2.added - c1.added); if (gtype == "corr") { + if (mycolor == 'w') game.chatRead = game.chatReadWhite; + else if (mycolor == 'b') game.chatRead = game.chatReadBlack; // NOTE: clocks in seconds game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of game.clocks = [tc.mainTime, tc.mainTime]; @@ -1042,33 +1058,10 @@ export default { (Date.now() - game.moves[L-1].played) / 1000; } } - if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) { - // Did a chat message arrive after my last move? - let dtLastMove = 0; - if (L == 1 && myIdx == 0) - dtLastMove = game.moves[0].played; - else if (L >= 2) { - if (L % 2 == 0) { - // It's now white turn - dtLastMove = game.moves[L-1-(1-myIdx)].played; - } else { - // Black turn: - dtLastMove = game.moves[L-1-myIdx].played; - } - } - if (dtLastMove < game.chats[0].added) - document.getElementById("chatBtn").classList.add("somethingnew"); - } // Now that we used idx and played, re-format moves as for live games game.moves = game.moves.map(m => m.squares); } - if (gtype == "live") { - if ( - game.chats.length > 0 && - (!game.initime || game.initime < game.chats[0].added) - ) { - document.getElementById("chatBtn").classList.add("somethingnew"); - } + else if (gtype == "live") { if (game.clocks[0] < 0) { // Game is unstarted. clock is ignored until move 2 game.clocks = [tc.mainTime, tc.mainTime]; @@ -1084,6 +1077,21 @@ export default { game.clocks[myIdx] -= (Date.now() - game.initime) / 1000; } } + else + // gtype == "import" + game.clocks = [tc.mainTime, tc.mainTime]; + // Live games before 26/03/2020 don't have chat history: + if (!game.chats) game.chats = []; //TODO: remove line + // Sort chat messages from newest to oldest + game.chats.sort((c1, c2) => c2.added - c1.added); + if ( + myIdx >= 0 && + game.chats.length > 0 && + (!game.chatRead || game.chatRead < game.chats[0].added) + ) { + // A chat message arrived since my last reading: + document.getElementById("chatBtn").classList.add("somethingnew"); + } // TODO: merge next 2 "if" conditions if (!!game.drawOffer) { if (game.drawOffer == "t") @@ -1117,15 +1125,17 @@ export default { } this.repeat = {}; //reset: scan past moves' FEN: let repIdx = 0; - let vr_tmp = new V(game.fenStart); + this.vr = new V(game.fenStart); let curTurn = "n"; game.moves.forEach(m => { - playMove(m, vr_tmp); - const fenIdx = vr_tmp.getFen().replace(/ /g, "_"); + playMove(m, this.vr); + const fenIdx = this.vr.getFenForRepeat(); this.repeat[fenIdx] = this.repeat[fenIdx] ? this.repeat[fenIdx] + 1 : 1; }); + // Imported games don't have current FEN + if (!game.fen) game.fen = this.vr.getFen(); if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep"; this.game = Object.assign( // NOTE: assign mycolor here, since BaseGame could also be VS computer @@ -1179,6 +1189,11 @@ export default { // - from server (one correspondance game I play[ed] or not) // - from remote peer (one live game I don't play, finished or not) fetchGame: function(callback) { + +console.log("fecth"); + console.log(this.gameRef); + console.log(this.gameRef.match(/^i/)); + if (Number.isInteger(this.gameRef) || !isNaN(parseInt(this.gameRef))) { // corr games identifiers are integers ajax( @@ -1195,7 +1210,7 @@ export default { } ); } - else if (!!this.gameRef.match(/^I_/)) + else if (!!this.gameRef.match(/^i/)) // Game import (maybe remote) ImportgameStorage.get(this.gameRef, callback); else @@ -1238,6 +1253,9 @@ export default { }, // Update variables and storage after a move: processMove: function(move, data) { + if (this.game.type == "import") + // Shouldn't receive any messages in this mode: + return; if (!data) data = {}; const moveCol = this.vr.turn; const colorIdx = ["w", "b"].indexOf(moveCol); diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index 890d9395..52e6dc3b 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -153,12 +153,19 @@ export default { // Now ask completed games (partial list) this.loadMore( "live", - () => this.loadMore("corr", adjustAndSetDisplay) + () => this.loadMore("corr", () => { + this.loadMore("import", adjustAndSetDisplay); + }) ); } } ); - } else this.loadMore("live", adjustAndSetDisplay); + } + else { + this.loadMore("live", () => { + this.loadMore("import", adjustAndSetDisplay); + }); + } }); }, beforeDestroy: function() { @@ -184,11 +191,17 @@ export default { } }, addGameImport(game) { - if (!game.id) { - alert(this.st.tr[ - "No identifier found: use the upload button in analysis mode"]); - } - else this.importGames.push(game); + game.type = "import"; + ImportgameStorage.add(game, (err) => { + if (!!err) { + if (err.message.indexOf("Key already exists") < 0) { + alert(this.st.tr["An error occurred. Try again!"]); + return; + } + else alert(this.st.tr["The game was already imported"]); + } + this.$router.push("/game/" + game.id); + }); }, tryShowNewsIndicator: function(type) { if ( diff --git a/server/db/create.sql b/server/db/create.sql index 7c361b47..56ae4f19 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -68,6 +68,8 @@ create table Games ( rematchOffer character default '', deletedByWhite boolean, deletedByBlack boolean, + chatReadWhite datetime, + chatReadBlack datetime, foreign key (vid) references Variants(id), foreign key (white) references Users(id), foreign key (black) references Users(id) diff --git a/server/models/Game.js b/server/models/Game.js index aec80a01..ee381d42 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -18,6 +18,8 @@ const UserModel = require("./User"); * randomness: integer * deletedByWhite: boolean * deletedByBlack: boolean + * chatReadWhite: datetime + * chatReadBlack: datetime * * Structure table Moves: * gid: ref game id @@ -74,6 +76,7 @@ const GameModel = "SELECT " + "g.id, g.fen, g.fenStart, g.cadence, g.created, " + "g.white, g.black, g.score, g.scoreMsg, " + + "g.chatReadWhite, g.chatReadBlack, " + "g.drawOffer, g.rematchOffer, v.name AS vname " + "FROM Games g " + "JOIN Variants v " + @@ -309,6 +312,8 @@ const GameModel = !obj.fen || !!(obj.fen.match(/^[a-zA-Z0-9, /-]*$/)) ) && ( !obj.score || !!(obj.score.match(/^[012?*\/-]+$/)) + ) && ( + !obj.chatRead || !(['w','b'].includes(obj.chatRead)) ) && ( !obj.scoreMsg || !!(obj.scoreMsg.match(/^[a-zA-Z ]+$/)) ) && ( @@ -343,6 +348,10 @@ const GameModel = const myColor = obj.deletedBy == 'w' ? "White" : "Black"; modifs += "deletedBy" + myColor + " = true,"; } + if (!!obj.chatRead) { + const myColor = obj.chatRead == 'w' ? "White" : "Black"; + modifs += "chatRead" + myColor + " = " + Date.now() + ","; + } if (!!obj.score) { modifs += "score = '" + obj.score + "'," + "scoreMsg = '" + obj.scoreMsg + "',"; -- 2.44.0