https://www.flaticon.com/free-icon/forward_2413353?term=forward&page=1&position=59
https://www.flaticon.com/free-icon/right_565870?term=forward&page=1&position=31
https://www.flaticon.com/free-icon/download_724933?term=download&page=1&position=3
+https://www.flaticon.com/free-icon/upload_725008?term=upload&page=1&position=14
https://www.flaticon.com/free-icon/resize_512182?term=resize&page=1&position=49
https://www.flaticon.com/free-icon/clear_565313?term=delete&page=1&position=33
https://www.flaticon.com/free-icon/clear_1632708?term=delete&page=1&position=3
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Capa_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 512 512"
+ style="enable-background:new 0 0 512 512;"
+ xml:space="preserve"
+ sodipodi:docname="upload.svg"
+ inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
+ id="metadata49"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs47" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="960"
+ inkscape:window-height="1060"
+ id="namedview45"
+ showgrid="false"
+ inkscape:zoom="1.3037281"
+ inkscape:cx="260.33898"
+ inkscape:cy="256"
+ inkscape:window-x="0"
+ inkscape:window-y="20"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="Capa_1" />
+<g
+ id="g6"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+ <g
+ id="g4">
+ <path
+ d="m 380.032,133.472 -112,-128 C 264.992,2.016 260.608,0 256,0 c -4.608,0 -8.992,2.016 -12.032,5.472 l -112,128 c -4.128,4.736 -5.152,11.424 -2.528,17.152 2.592,5.696 8.288,9.376 14.56,9.376 h 64 v 208 c 0,8.832 7.168,16 16,16 h 64 c 8.832,0 16,-7.168 16,-16 V 160 h 64 c 6.272,0 11.968,-3.648 14.56,-9.376 2.592,-5.728 1.632,-12.448 -2.528,-17.152 z"
+ id="path2"
+ inkscape:connector-curvature="0" />
+ </g>
+</g>
+<g
+ id="g12"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+ <g
+ id="g10">
+ <path
+ d="m 432,352 v 96 H 80 V 352 H 16 v 128 c 0,17.696 14.336,32 32,32 h 416 c 17.696,0 32,-14.304 32,-32 V 352 Z"
+ id="path8"
+ inkscape:connector-curvature="0" />
+ </g>
+</g>
+<g
+ id="g14"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g16"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g18"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g20"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g22"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g24"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g26"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g28"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g30"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g32"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g34"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g36"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g38"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g40"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+<g
+ id="g42"
+ transform="matrix(1.0639192,0,0,1,-15.596299,0)">
+</g>
+</svg>
\ No newline at end of file
elementArray.push(gameDiv);
if (!!this.vr.reserve) elementArray.push(reserveBottom);
const boardElt = document.querySelector(".game");
- // Square width might be undefine (at first drawing),
+ // boardElt might be undefine (at first drawing),
// but it won't be used in this case.
- const squareWidth = boardElt.offsetWidth / sizeY;
+ const squareWidth = (!!boardElt ? boardElt.offsetWidth / sizeY : 42);
if (this.choices.length > 0 && !!boardElt) {
// No choices to show at first drawing
const offset = [boardElt.offsetTop, boardElt.offsetLeft];
attrs: {
id: "arrow",
markerWidth: (2 * arrowWidth) + "px",
- markerHeight: (2 * arrowWidth) + "px",
+ markerHeight: (3 * arrowWidth) + "px",
markerUnits: "userSpaceOnUse",
refX: "0",
- refY: arrowWidth + "px",
+ refY: (1.5 * arrowWidth) + "px",
orient: "auto"
}
},
"class": { "arrow-head": true },
attrs: {
d: (
- "M0,0 L0," + (2 * arrowWidth) + " " +
- "L" + (2 * arrowWidth) + "," + arrowWidth + " z"
+ "M0,0 L0," + (3 * arrowWidth) + " L" +
+ (2 * arrowWidth) + "," + (1.5 * arrowWidth) + " z"
)
}
}
<script>
import { store } from "@/store";
import { GameStorage } from "@/utils/gameStorage";
+import { ImportgameStorage } from "@/utils/importgameStorage";
import { ajax } from "@/utils/ajax";
export default {
name: "my-game-list",
: "Abort and remove game?";
if (confirm(this.st.tr[message])) {
const afterDelete = () => {
- if (game.score == "*") this.$emit("abortgame", game);
+ if (game.score == "*" && game.type != "import")
+ this.$emit("abortgame", game);
this.$set(this.deleted, game.id, true);
};
if (game.type == "live")
// Effectively remove game:
GameStorage.remove(game.id, afterDelete);
+ else if (game.type == "import")
+ ImportgameStorage.remove(game.id, afterDelete);
else {
const mySide =
game.players[0].id == this.st.user.id
--- /dev/null
+<template lang="pug">
+div
+ input#upload(type="file" @change="upload")
+ button#uploadBtn(
+ @click="uploadTrigger()"
+ aria-label="store.state.tr['Upload a game']"
+ )
+ img.inline(src="/images/icons/upload.svg")
+</template>
+
+<script>
+export default {
+ name: "my-upload-game",
+ methods: {
+ uploadTrigger: function() {
+ document.getElementById("upload").click();
+ },
+ upload: function(e) {
+ const file = (e.target.files || e.dataTransfer.files)[0];
+ var reader = new FileReader();
+ reader.onloadend = ev => {
+ this.parseAndEmit(ev.currentTarget.result);
+ };
+ 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);
+ }
+ }
+};
+</script>
+
+<style lang="sass" scoped>
+input#upload
+ display: none
+
+img.inline
+ height: 22px
+ @media screen and (max-width: 767px)
+ height: 18px
+</style>
green: "green",
Hall: "Hall",
"Highlight last move": "Highlight last move",
+ "Imported games": "Imported games",
Instructions: "Instructions",
"Invalid email": "Invalid email",
"It's your turn!": "It's your turn!",
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",
Time: "Time",
"Undetermined result": "Undetermined result",
Update: "Update",
+ "Upload a game": "Upload a game",
"User creation failed. Try again": "User creation failed. Try again",
"User name": "User name",
"User name or email already in use": "User name or email already in use",
green: "verde",
Hall: "Salón",
"Highlight last move": "Resaltar el último movimiento",
+ "Imported games": "Partidas importadas",
Instructions: "Instrucciones",
"Invalid email": "Email inválido",
"It's your turn!": "¡Es su turno!",
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",
Time: "Tiempo",
"Undetermined result": "Resultado indeterminado",
Update: "Actualización",
+ "Upload a game": "Enviar una partida",
"User creation failed. Try again": "Error al crear cuenta. inténtelo de nuevo",
"User name": "Nombre de usuario",
"User name or email already in use": "Nombre de usuario o correo electrónico ya en uso",
green: "vert",
Hall: "Salon",
"Highlight last move": "Mettre en valeur le dernier coup",
+ "Imported games": "Parties importées",
Instructions: "Instructions",
"Invalid email": "Email invalide",
"It's your turn!": "À vous de jouer !",
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",
Time: "Temps",
"Undetermined result": "Résultat indéterminé",
Update: "Mise à jour",
+ "Upload a game": "Envoyer une partie",
"User creation failed. Try again": "Échec de la création du compte. Réessayez",
"User name": "Nom d'utilisateur",
"User name or email already in use": "Nom d'utilisateur ou email déjà utilisés",
export const CompgameStorage = {
add: function(game) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
if (err) return;
let objectStore = db
.transaction("compgames", "readwrite")
// obj: move and/or fen
update: function(gameId, obj) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
let objectStore = db
.transaction("compgames", "readwrite")
.objectStore("compgames");
// Retrieve any game from its identifier (variant name)
// NOTE: need callback because result is obtained asynchronously
get: function(gameId, callback) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
let objectStore = db
.transaction("compgames", "readonly")
.objectStore("compgames");
// Delete a game in indexedDB
remove: function(gameId) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
if (!err) {
db.transaction("compgames", "readwrite")
.objectStore("compgames")
export const GameStorage = {
// Optional callback to get error status
add: function(game, callback) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
if (!!err) {
callback("error");
return;
// obj: chat, move, fen, clocks, score[Msg], initime, ...
update: function(gameId, obj) {
// live
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
let objectStore = db
.transaction("games", "readwrite")
.objectStore("games");
// Retrieve (all) running local games
getRunning: function(callback) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
let objectStore = db
.transaction("games", "readonly")
.objectStore("games");
// Retrieve completed local games
getNext: function(upperDt, callback) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
let objectStore = db
.transaction("games", "readonly")
.objectStore("games");
});
},
- // Retrieve any game from its identifiers (locally or on server)
+ // Retrieve any game from its identifier.
// NOTE: need callback because result is obtained asynchronously
get: function(gameId, callback) {
- // Local game
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
let objectStore = db.transaction("games").objectStore("games");
objectStore.get(gameId).onsuccess = function(event) {
// event.target.result is null if game not found
// Delete a game in indexedDB
remove: function(gameId, callback) {
- dbOperation((err,db) => {
+ dbOperation((err, db) => {
if (!err) {
let transaction = db.transaction("games", "readwrite");
transaction.oncomplete = function() {
--- /dev/null
+// Game object struct: see gameStorgae.js
+
+import { store } from "@/store";
+
+function dbOperation(callback) {
+ let db = null;
+ let DBOpenRequest = window.indexedDB.open("vchess_import", 4);
+
+ DBOpenRequest.onerror = function(event) {
+ alert(store.state.tr[
+ "Database error: stop private browsing, or update your browser"]);
+ callback("error", null);
+ };
+
+ DBOpenRequest.onsuccess = function() {
+ db = DBOpenRequest.result;
+ callback(null, db);
+ db.close();
+ };
+
+ DBOpenRequest.onupgradeneeded = function(event) {
+ let db = event.target.result;
+ let upgradeTransaction = event.target.transaction;
+ let objectStore = undefined;
+ if (!db.objectStoreNames.contains("importgames"))
+ objectStore = db.createObjectStore("importgames", { keyPath: "id" });
+ else
+ objectStore = upgradeTransaction.objectStore("importgames");
+ if (!objectStore.indexNames.contains("created"))
+ // To search by date intervals. Two games could start at the same time
+ objectStore.createIndex("created", "created", { unique: false });
+ };
+}
+
+export const ImportgameStorage = {
+ // Optional callback to get error status
+ add: function(game, callback) {
+ dbOperation((err, db) => {
+ if (!!err) {
+ callback("error");
+ return;
+ }
+ let transaction = db.transaction("importgames", "readwrite");
+ transaction.oncomplete = function() {
+ // Everything's fine
+ callback();
+ };
+ transaction.onerror = function(err) {
+ // Duplicate key error (most likely)
+ callback(err);
+ };
+ transaction.objectStore("importgames").add(game);
+ });
+ },
+
+ // Retrieve next imported games
+ getNext: function(upperDt, callback) {
+ dbOperation((err, db) => {
+ let objectStore = db
+ .transaction("importgames", "readonly")
+ .objectStore("importgames");
+ let index = objectStore.index("created");
+ const range = IDBKeyRange.upperBound(upperDt);
+ let games = [];
+ index.openCursor(range).onsuccess = function(event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ // Most recent games first:
+ games = games.sort((g1, g2) => g2.created - g1.created);
+ // TODO: 20 games showed per request is arbitrary
+ callback(games.slice(0, 20));
+ }
+ else {
+ // If there is still another cursor to go, keep running this code
+ let g = cursor.value;
+ // Do not retrieve moves or clocks (unused in list mode)
+ g.movesCount = g.moves.length;
+ delete g.moves;
+ delete g.clocks;
+ delete g.initime;
+ games.push(g);
+ cursor.continue();
+ }
+ };
+ });
+ },
+
+ // Retrieve any game from its identifier.
+ // NOTE: need callback because result is obtained asynchronously
+ get: function(gameId, callback) {
+ dbOperation((err, db) => {
+ let objectStore =
+ db.transaction("importgames").objectStore("importgames");
+ objectStore.get(gameId).onsuccess = function(event) {
+ // event.target.result is null if game not found
+ callback(event.target.result);
+ };
+ });
+ },
+
+ // Delete a game in indexedDB
+ remove: function(gameId, callback) {
+ dbOperation((err, db) => {
+ if (!err) {
+ let transaction = db.transaction("importgames", "readwrite");
+ transaction.oncomplete = function() {
+ callback(); //everything's fine
+ };
+ transaction.objectStore("importgames").delete(gameId);
+ }
+ });
+ }
+};
import { store } from "@/store";
export default {
name: "my-analyse",
+ // TODO: game import ==> require some adjustments, like
+ // the ability to analyse from a list of moves...
components: {
BaseGame
},
return {
st: store.state,
gameRef: {
- //given in URL (rid = remote ID)
vname: "",
fen: ""
},
import Chat from "@/components/Chat.vue";
import { store } from "@/store";
import { GameStorage } from "@/utils/gameStorage";
+import { ImportgameStorage } from "@/utils/importgameStorage";
import { ppt } from "@/utils/datetime";
import { notify } from "@/utils/notifications";
import { ajax } from "@/utils/ajax";
}
}
);
- } else
- // Local game (or live remote)
+ }
+ else if (!!this.gameRef.match(/^I_/))
+ // Game import (maybe remote)
+ ImportgameStorage.get(this.gameRef, callback);
+ else
+ // Local live game (or remote)
GameStorage.get(this.gameRef, callback);
},
re_setClocks: function() {
| {{ st.tr["Live games"] }}
button.tabbtn#corrGames(@click="setDisplay('corr',$event)")
| {{ st.tr["Correspondance games"] }}
+ button.tabbtn#importGames(@click="setDisplay('import',$event)")
+ | {{ st.tr["Imported games"] }}
GameList(
ref="livegames"
v-show="display=='live'"
@show-game="showGame"
@abortgame="abortGame"
)
+ GameList(
+ v-show="display=='import'"
+ ref="importgames"
+ :games="importGames"
+ @show-game="showGame"
+ )
button#loadMoreBtn(
v-show="hasMore[display]"
@click="loadMore(display)"
)
| {{ st.tr["Load more"] }}
+ UploadGame(@game-uploaded="addGameImport")
</template>
<script>
import { store } from "@/store";
import { GameStorage } from "@/utils/gameStorage";
+import { ImportgameStorage } from "@/utils/importgameStorage";
import { ajax } from "@/utils/ajax";
import { getScoreMessage } from "@/utils/scoring";
import params from "@/parameters";
export default {
name: "my-my-games",
components: {
- GameList
+ GameList,
+ UploadGame
},
data: function() {
return {
display: "live",
liveGames: [],
corrGames: [],
+ importGames: [],
// timestamp of last showed (oldest) game:
cursor: {
live: Number.MAX_SAFE_INTEGER,
+ "import": Number.MAX_SAFE_INTEGER,
corr: Number.MAX_SAFE_INTEGER
},
// hasMore == TRUE: a priori there could be more games to load
- hasMore: { live: true, corr: store.state.user.id > 0 },
+ hasMore: {
+ live: true,
+ "import": true,
+ corr: (store.state.user.id > 0)
+ },
conn: null,
connexionString: "",
socketCloseListener: 0
elt.previousElementSibling.classList.remove("active");
else elt.nextElementSibling.classList.remove("active");
},
+ addGameImport(game) {
+ if (!game.id) {
+ alert(this.st.tr[
+ "No identifier found: use the upload button in analysis mode"]);
+ }
+ else this.importGames.push(game);
+ },
tryShowNewsIndicator: function(type) {
if (
(type == "live" && this.display == "corr") ||
socketMessageListener: function(msg) {
if (!this.conn) return;
const data = JSON.parse(msg.data);
+ // NOTE: no imported games here
let gamesArrays = {
"corr": this.corrGames,
"live": this.liveGames
}
},
showGame: function(game) {
- if (game.type == "live" || !game.myTurn) {
+ if (game.type != "corr" || !game.myTurn) {
this.$router.push("/game/" + game.id);
return;
}
);
}
}
+ // NOTE: no imported games here
else if (!game.deletedByWhite || !game.deletedByBlack) {
// Set score if game isn't deleted on server:
ajax(
}
}
);
- } else if (type == "live") {
+ }
+ else if (type == "live") {
GameStorage.getNext(this.cursor["live"], localGames => {
const L = localGames.length;
if (L > 0) {
if (!!cb) cb();
});
}
+ else if (type == "import") {
+ ImportgameStorage.getNext(this.cursor["import"], importGames => {
+ const L = importGames.length;
+ if (L > 0) {
+ // Add "-1" because IDBKeyRange.upperBound includes boundary
+ this.cursor["import"] = importGames[L - 1].created - 1;
+ importGames.forEach(g => g.type = "import");
+ this.importGames = this.importGames.concat(importGames);
+ } else this.hasMore["import"] = false;
+ if (!!cb) cb();
+ });
+ }
}
}
};