From 5f918a278904266a2a66a3c8e2a3655f37c2d2a7 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 30 Mar 2020 15:06:08 +0200
Subject: [PATCH] Draft game upload logic (unwritten yet for Analysis mode)

---
 client/public/images/icons/SOURCE     |   1 +
 client/public/images/icons/upload.svg | 126 ++++++++++++++++++++++++++
 client/src/components/Board.vue       |  12 +--
 client/src/components/GameList.vue    |   6 +-
 client/src/components/UploadGame.vue  |  44 +++++++++
 client/src/translations/en.js         |   3 +
 client/src/translations/es.js         |   3 +
 client/src/translations/fr.js         |   3 +
 client/src/utils/compgameStorage.js   |   8 +-
 client/src/utils/gameStorage.js       |  15 ++-
 client/src/utils/importgameStorage.js | 113 +++++++++++++++++++++++
 client/src/views/Analyse.vue          |   3 +-
 client/src/views/Game.vue             |   9 +-
 client/src/views/MyGames.vue          |  47 +++++++++-
 14 files changed, 367 insertions(+), 26 deletions(-)
 create mode 100644 client/public/images/icons/upload.svg
 create mode 100644 client/src/components/UploadGame.vue
 create mode 100644 client/src/utils/importgameStorage.js

diff --git a/client/public/images/icons/SOURCE b/client/public/images/icons/SOURCE
index 4d22cabf..235d0b93 100644
--- a/client/public/images/icons/SOURCE
+++ b/client/public/images/icons/SOURCE
@@ -10,6 +10,7 @@ https://www.onlinewebfonts.com/icon/256756
 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
diff --git a/client/public/images/icons/upload.svg b/client/public/images/icons/upload.svg
new file mode 100644
index 00000000..4e8253a0
--- /dev/null
+++ b/client/public/images/icons/upload.svg
@@ -0,0 +1,126 @@
+<?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
diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue
index cf0a70c7..639938b2 100644
--- a/client/src/components/Board.vue
+++ b/client/src/components/Board.vue
@@ -303,9 +303,9 @@ export default {
     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];
@@ -446,10 +446,10 @@ export default {
                     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"
                     }
                   },
@@ -460,8 +460,8 @@ export default {
                         "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"
                           )
                         }
                       }
diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue
index a83eb85a..5596e448 100644
--- a/client/src/components/GameList.vue
+++ b/client/src/components/GameList.vue
@@ -28,6 +28,7 @@ div
 <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",
@@ -124,12 +125,15 @@ export default {
             : "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
diff --git a/client/src/components/UploadGame.vue b/client/src/components/UploadGame.vue
new file mode 100644
index 00000000..269cc04d
--- /dev/null
+++ b/client/src/components/UploadGame.vue
@@ -0,0 +1,44 @@
+<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>
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 7de0b9c1..e1d6189e 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -50,6 +50,7 @@ export const translations = {
   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!",
@@ -85,6 +86,7 @@ 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",
@@ -137,6 +139,7 @@ export const translations = {
   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",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 3a4ece50..d14f4606 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -50,6 +50,7 @@ export const translations = {
   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!",
@@ -85,6 +86,7 @@ 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",
@@ -137,6 +139,7 @@ export const translations = {
   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",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 3ed54e76..2d7b535b 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -50,6 +50,7 @@ export const translations = {
   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 !",
@@ -85,6 +86,7 @@ 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",
@@ -137,6 +139,7 @@ export const translations = {
   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",
diff --git a/client/src/utils/compgameStorage.js b/client/src/utils/compgameStorage.js
index e30a69fb..655fd6bf 100644
--- a/client/src/utils/compgameStorage.js
+++ b/client/src/utils/compgameStorage.js
@@ -38,7 +38,7 @@ function dbOperation(callback) {
 
 export const CompgameStorage = {
   add: function(game) {
-    dbOperation((err,db) => {
+    dbOperation((err, db) => {
       if (err) return;
       let objectStore = db
         .transaction("compgames", "readwrite")
@@ -49,7 +49,7 @@ export const CompgameStorage = {
 
   // obj: move and/or fen
   update: function(gameId, obj) {
-    dbOperation((err,db) => {
+    dbOperation((err, db) => {
       let objectStore = db
         .transaction("compgames", "readwrite")
         .objectStore("compgames");
@@ -70,7 +70,7 @@ export const CompgameStorage = {
   // 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");
@@ -82,7 +82,7 @@ export const CompgameStorage = {
 
   // Delete a game in indexedDB
   remove: function(gameId) {
-    dbOperation((err,db) => {
+    dbOperation((err, db) => {
       if (!err) {
         db.transaction("compgames", "readwrite")
           .objectStore("compgames")
diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js
index 62ad1fab..109c8251 100644
--- a/client/src/utils/gameStorage.js
+++ b/client/src/utils/gameStorage.js
@@ -53,7 +53,7 @@ function dbOperation(callback) {
 export const GameStorage = {
   // Optional callback to get error status
   add: function(game, callback) {
-    dbOperation((err,db) => {
+    dbOperation((err, db) => {
       if (!!err) {
         callback("error");
         return;
@@ -74,7 +74,7 @@ export const GameStorage = {
   // 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");
@@ -98,7 +98,7 @@ export const GameStorage = {
 
   // Retrieve (all) running local games
   getRunning: function(callback) {
-    dbOperation((err,db) => {
+    dbOperation((err, db) => {
       let objectStore = db
         .transaction("games", "readonly")
         .objectStore("games");
@@ -125,7 +125,7 @@ export const GameStorage = {
 
   // Retrieve completed local games
   getNext: function(upperDt, callback) {
-    dbOperation((err,db) => {
+    dbOperation((err, db) => {
       let objectStore = db
         .transaction("games", "readonly")
         .objectStore("games");
@@ -157,11 +157,10 @@ export const GameStorage = {
     });
   },
 
-  // 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
@@ -172,7 +171,7 @@ export const GameStorage = {
 
   // 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() {
diff --git a/client/src/utils/importgameStorage.js b/client/src/utils/importgameStorage.js
new file mode 100644
index 00000000..e5ad6e22
--- /dev/null
+++ b/client/src/utils/importgameStorage.js
@@ -0,0 +1,113 @@
+// 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);
+      }
+    });
+  }
+};
diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue
index 1605b584..2f087b99 100644
--- a/client/src/views/Analyse.vue
+++ b/client/src/views/Analyse.vue
@@ -19,6 +19,8 @@ import BaseGame from "@/components/BaseGame.vue";
 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
   },
@@ -27,7 +29,6 @@ export default {
     return {
       st: store.state,
       gameRef: {
-        //given in URL (rid = remote ID)
         vname: "",
         fen: ""
       },
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 68afce35..2a565f68 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -153,6 +153,7 @@ import BaseGame from "@/components/BaseGame.vue";
 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";
@@ -1193,8 +1194,12 @@ export default {
             }
           }
         );
-      } 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() {
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index 3c3b9b5a..7522871f 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -7,6 +7,8 @@ main
           | {{ 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'"
@@ -21,16 +23,24 @@ main
         @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";
@@ -39,7 +49,8 @@ import GameList from "@/components/GameList.vue";
 export default {
   name: "my-my-games",
   components: {
-    GameList
+    GameList,
+    UploadGame
   },
   data: function() {
     return {
@@ -47,13 +58,19 @@ export default {
       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
@@ -161,6 +178,13 @@ export default {
         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") ||
@@ -190,6 +214,7 @@ export default {
     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
@@ -240,7 +265,7 @@ export default {
       }
     },
     showGame: function(game) {
-      if (game.type == "live" || !game.myTurn) {
+      if (game.type != "corr" || !game.myTurn) {
         this.$router.push("/game/" + game.id);
         return;
       }
@@ -277,6 +302,7 @@ export default {
           );
         }
       }
+      // NOTE: no imported games here
       else if (!game.deletedByWhite || !game.deletedByBlack) {
         // Set score if game isn't deleted on server:
         ajax(
@@ -315,7 +341,8 @@ export default {
             }
           }
         );
-      } else if (type == "live") {
+      }
+      else if (type == "live") {
         GameStorage.getNext(this.cursor["live"], localGames => {
           const L = localGames.length;
           if (L > 0) {
@@ -328,6 +355,18 @@ export default {
           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();
+        });
+      }
     }
   }
 };
-- 
2.44.0