changeset 2727:03edc9db9f8c

Merge branch 'master' of https://github.com/BrechtDeMan/WebAudioEvaluationTool
author www-data <www-data@sucuk.dcs.qmul.ac.uk>
date Sat, 15 Apr 2017 12:20:51 +0100
parents 2bc86ec0ac6c (current diff) c74c698795a9 (diff)
children e9c5cf559ecf
files
diffstat 11 files changed, 235 insertions(+), 65 deletions(-) [+]
line wrap: on
line diff
--- a/css/core.css	Fri Apr 14 17:21:11 2017 +0100
+++ b/css/core.css	Sat Apr 15 12:20:51 2017 +0100
@@ -8,6 +8,7 @@
     margin-bottom: 10px;
     font-size: 2em;
 }
+
 div.indicator-box {
     position: absolute;
     left: 150px;
@@ -18,6 +19,7 @@
     border-radius: 10px;
     background-color: rgb(100, 200, 200);
 }
+
 div.comment-div {
     border: 1px solid #444444;
     max-width: 600px;
@@ -27,9 +29,11 @@
     height: 90px;
     border-radius: 10px;
 }
+
 div.comment-div span {
     margin-left: 15px;
 }
+
 div.popupHolder {
     width: 500px;
     min-height: 250px;
@@ -40,16 +44,19 @@
     z-index: 10;
     position: fixed;
 }
+
 div#popupContent {
     margin-top: 20px;
     margin-bottom: 35px;
     overflow: auto;
 }
+
 div#popupContent iframe {
     width: 100%;
     border: 0px none;
     height: 290px;
 }
+
 div#popupTitleHolder {
     width: inherit;
     min-height: 25px;
@@ -59,9 +66,11 @@
     padding: 8px;
     text-align: center;
 }
+
 #popupTitle {
     white-space: pre-line;
 }
+
 div#popupResponse {
     width: inherit;
     min-height: 50px;
@@ -69,6 +78,7 @@
     overflow: auto;
     position: relative;
 }
+
 button.popupButton {
     /* Button for popup window
 	 */
@@ -81,6 +91,7 @@
     border-style: solid;
     background-color: #fff;
 }
+
 div.popup-option-checbox {
     /* Popup window checkbox */
     padding: 5px;
@@ -88,33 +99,41 @@
     width: -moz-fit-content;
     width: -webkit-fit-content;
 }
+
 div.popup-option-checbox input {
     /* Popup window checkbox */
     margin-right: 15px;
 }
+
 table.popup-option-list {
     margin: auto;
 }
+
 table.popup-option-list tr {
     padding: 5px;
 }
+
 table.popup-option-list tr td {
     padding: 5px;
 }
+
 div.survey-slider-text-holder {
     display: flex;
     flex-direction: row;
     justify-content: space-between;
     padding: 0px 15px;
 }
+
 button#popup-proceed {
     bottom: 10px;
     right: 10px;
 }
+
 button#popup-previous {
     bottom: 10px;
     left: 10px;
 }
+
 div.testHalt {
     /* Specify any colouring during the test halt for pre/post questions */
     background-color: rgba(0, 0, 0, 0.5);
@@ -126,6 +145,7 @@
     left: 0px;
     top: 0px;
 }
+
 div#lightbox-root {
     visibility: hidden;
     z-index: 20;
@@ -133,6 +153,7 @@
     min-height: 50px;
     max-height: 250px;
 }
+
 div.lightbox-error {
     margin: 25px;
     margin-bottom: 50px;
@@ -141,6 +162,7 @@
     background-color: rgb(255, 220, 220);
     border: 2px rgb(200, 0, 0) solid;
 }
+
 div.lightbox-warning {
     margin: 25px;
     margin-bottom: 50px;
@@ -149,6 +171,7 @@
     background-color: rgb(255, 255, 220);
     border: 2px rgb(255, 250, 0) solid;
 }
+
 div.lightbox-message {
     margin: 25px;
     margin-bottom: 50px;
@@ -157,22 +180,26 @@
     background-color: rgb(200, 220, 255);
     border: 2px rgb(50, 100, 250) solid;
 }
+
 div#lightbox-blanker {
     visibility: hidden;
     z-index: 19;
 }
+
 button.outside-reference {
     width: 120px;
     height: 20px;
     margin-bottom: 5px;
     position: absolute;
 }
+
 textarea.trackComment {
     max-width: 594px;
     min-width: 350px;
     max-height: 60px;
     resize: none;
 }
+
 div.playhead {
     width: 500px;
     height: 50px;
@@ -180,22 +207,26 @@
     border-radius: 10px;
     padding: 10px;
 }
+
 div.playhead-scrub-track {
     width: 100%;
     height: 10px;
     border-style: solid;
     border-width: 1px;
 }
+
 div#playhead-scrubber {
     width: 10px;
     height: 10px;
     position: relative;
     background-color: #000;
 }
+
 div.master-volume-holder-inline {
     width: 100%;
     padding: 5px;
 }
+
 div.master-volume-holder-float {
     position: absolute;
     top: 20px;
@@ -203,6 +234,7 @@
     width: 250px%;
     padding: 5px;
 }
+
 div#master-volume-root {
     margin: auto;
     border: black 1px solid;
@@ -210,6 +242,7 @@
     width: 250px;
     height: 40px;
 }
+
 input#master-volume-control {
     width: 190px;
     height: 25px;
@@ -217,23 +250,28 @@
     margin: 0px;
     padding: 0px;
 }
+
 span#master-volume-feedback {
     height: 25px;
     margin: 0px 5px;
     float: right;
 }
+
 div.error-colour {
     background-color: #FF8F8F;
 }
+
 button.error-colour {
     background-color: #FF8F8F;
     color: black;
 }
+
 div.calibration-holder {
     text-align: center;
     align-content: center;
     height: auto;
 }
+
 div.calibration-slider {
     width: 50px;
     margin: 2px;
@@ -241,6 +279,7 @@
     align-content: center;
     float: left;
 }
+
 div.calibration-slider input[type=range][orient=vertical] {
     writing-mode: bt-lr;
     /* IE */
@@ -251,6 +290,7 @@
     height: 290px;
 }
 
+
 /*  Comment Boxes */
 
 div.comment-slider-text-holder {
@@ -258,18 +298,25 @@
     flex-direction: row;
     justify-content: space-between;
 }
+
 div.comment-slider-text-holder span {
     margin: 0px 5px;
 }
+
 div.comment-checkbox-inputs-holder {
     display: flex;
     flex-direction: row;
     justify-content: space-around;
     margin: 10px 5px;
 }
+
 div.comment-checkbox-inputs-flex {
     display: flex;
     flex-direction: column;
     justify-content: space-between;
     align-items: center;
-}
\ No newline at end of file
+}
+
+div.comment-box-playing {
+    background-color: #FFDDDD;
+}
--- a/interfaces/AB.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/AB.js	Sat Apr 15 12:20:51 2017 +0100
@@ -294,6 +294,7 @@
             }
             $(this.playback).text('Stop');
             this.playback.setAttribute("playstate", "playing");
+            interfaceContext.commentBoxes.highlightById(audioElement.id);
         };
         this.stopPlayback = function () {
             if (this.playback.getAttribute("playstate") == "playing") {
@@ -301,6 +302,12 @@
                 $('.comparator-button').removeAttr("disabled");
                 this.playback.setAttribute("playstate", "ready");
             }
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === audioElement.id;
+            });
+            if (box) {
+                box.highlight(false);
+            }
         };
         this.exportXMLDOM = function (audioObject) {
             var node = storage.document.createElement('value');
--- a/interfaces/ABX.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/ABX.js	Sat Apr 15 12:20:51 2017 +0100
@@ -280,6 +280,7 @@
             }
             $(this.playback).text('Stop');
             this.playback.setAttribute("playstate", "playing");
+            interfaceContext.commentBoxes.highlightById(element.id);
         };
         this.stopPlayback = function () {
             if (this.playback.getAttribute("playstate") == "playing") {
@@ -287,6 +288,12 @@
                 $('.comparator-button').removeAttr("disabled");
                 this.playback.setAttribute("playstate", "ready");
             }
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === element.id;
+            });
+            if (box) {
+                box.highlight(false);
+            }
         };
         this.getValue = function () {
             // Return the current value of the object. If there is no value, return 0
--- a/interfaces/ape.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/ape.js	Sat Apr 15 12:20:51 2017 +0100
@@ -590,8 +590,7 @@
         $('.track-slider').removeClass('track-slider-playing');
         var name = ".track-slider-" + this.parent.id;
         $(name).addClass('track-slider-playing');
-        $('.comment-div').removeClass('comment-box-playing');
-        $('#comment-div-' + this.parent.id).addClass('comment-box-playing');
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
         $('.outside-reference').removeClass('track-slider-playing');
         this.playing = true;
 
@@ -605,9 +604,14 @@
             this.playing = false;
             var name = ".track-slider-" + this.parent.id;
             $(name).removeClass('track-slider-playing');
-            $('#comment-div-' + this.parent.id).removeClass('comment-box-playing');
             $('.track-slider').removeClass('track-slider-disabled');
             $('.outside-reference').removeClass('track-slider-disabled');
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === audioObject.id;
+            });
+            if (box) {
+                box.highlight(false);
+            }
         }
     };
     this.exportXMLDOM = function (audioObject) {
--- a/interfaces/discrete.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/discrete.js	Sat Apr 15 12:20:51 2017 +0100
@@ -338,6 +338,7 @@
             $('.track-slider-button').text = "Wait";
             $('.track-slider-button').attr("disabled", "true");
         }
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
     };
     this.stopPlayback = function () {
         // Called by audioObject when playback stops
@@ -347,6 +348,12 @@
             $('.track-slider-button').text = "Play";
             this.play.textContent = "Play";
             $('.track-slider-button').removeAttr("disabled");
+            var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+                return a.id === audioObject.id;
+            });
+            if (box) {
+                box.highlight(false);
+            }
         }
     };
 
--- a/interfaces/horizontal-sliders.css	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/horizontal-sliders.css	Sat Apr 15 12:20:51 2017 +0100
@@ -7,19 +7,23 @@
     /* Set the background colour (note US English spelling) to grey*/
     background-color: #ddd
 }
+
 div.pageTitle {
     width: auto;
     height: 20px;
     margin: 10px 0px;
 }
+
 div.pageTitle span {
     font-size: 1.5em;
 }
+
 button {
     /* Specify any button structure or style */
     min-width: 20px;
     background-color: #ddd
 }
+
 div#slider-holder {
     height: inherit;
     position: absolute;
@@ -27,24 +31,29 @@
     z-index: 3;
     margin-top: 25px;
 }
+
 div#scale-holder {
     height: inherit;
     position: absolute;
     left: 0px;
     z-index: 2;
 }
+
 div#scale-text-holder {
     position: relative;
     float: left;
 }
+
 div.scale-text {
     position: absolute;
     font-size: 1.2em;
 }
+
 canvas#scale-canvas {
     position: relative;
     float: left;
 }
+
 div.track-slider {
     float: left;
     height: 94px;
@@ -55,44 +64,53 @@
     margin-left: 94px;
     margin-bottom: 25px;
 }
+
 div.track-slider-title {
     float: left;
     padding-top: 40px;
     width: 100px;
 }
+
 button.track-slider-button {
     float: left;
     width: 100px;
     height: 94px;
 }
+
 div#outside-reference-holder {
     display: flex;
     align-content: center;
     justify-content: center;
     margin-bottom: 5px;
 }
+
 button.outside-reference {
     position: inherit;
     margin: 0px 5px;
 }
+
 div.track-slider-playing {
     background-color: #FFDDDD;
 }
+
 input.track-slider-range {
     float: left;
     margin: 2px 10px;
 }
+
 input[type=range] {
     height: 94px;
     padding: 0px;
     color: rgb(255, 144, 144);
 }
+
 input[type=range]::-webkit-slider-runnable-track {
     cursor: pointer;
     background: #fff;
     border-radius: 4px;
     border: 1px solid #000;
 }
+
 input[type=range]::-moz-range-track {
     height: 8px;
     cursor: pointer;
@@ -100,18 +118,26 @@
     border-radius: 4px;
     border: 1px solid #000;
 }
+
 input.track-slider-not-moved[type=range]::-webkit-slider-runnable-track {
     background: #aaa;
 }
+
 input.track-slider-not-moved[type=range]::-moz-range-track {
     background: #aaa;
 }
+
 div#page-count {
     float: left;
     margin: 0px 5px;
 }
+
 div#master-volume-holder {
     position: absolute;
     top: 10px;
     left: 120px;
 }
+
+div.comment-box-playing {
+    background-color: #FFDDDD;
+}
--- a/interfaces/horizontal-sliders.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/horizontal-sliders.js	Sat Apr 15 12:20:51 2017 +0100
@@ -296,11 +296,18 @@
         if (outsideReference !== null) {
             $(outsideReference).removeClass('track-slider-playing');
         }
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
     };
     this.stopPlayback = function () {
         // Called when playback has stopped. This gets called even if playback never started!
         this.play.setAttribute("playstate", "ready");
         $(this.holder).removeClass('track-slider-playing');
+        var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+            return a.id === audioObject.id;
+        });
+        if (box) {
+            box.highlight(false);
+        }
     };
     this.getValue = function () {
         // Return the current value of the object. If there is no value, return 0
--- a/interfaces/mushra.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/mushra.js	Sat Apr 15 12:20:51 2017 +0100
@@ -316,6 +316,7 @@
         this.play.setAttribute("playstate", "playing");
         $(".track-slider").removeClass('track-slider-playing');
         $(this.holder).addClass('track-slider-playing');
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
         var outsideReference = document.getElementById('outside-reference');
         if (outsideReference !== null) {
             $(outsideReference).removeClass('track-slider-playing');
@@ -347,6 +348,12 @@
             $(this.slider).addClass("track-slider-range-disabled");
             this.slider.setAttribute("disabled", "true");
         }
+        var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+            return a.id === audioObject.id;
+        });
+        if (box) {
+            box.highlight(false);
+        }
     };
     this.getValue = function () {
         return this.slider.value;
--- a/interfaces/timeline.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/interfaces/timeline.js	Sat Apr 15 12:20:51 2017 +0100
@@ -430,12 +430,19 @@
         // Called when playback has begun
         canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas), 100);
         this.playButton.DOM.textContent = "Stop";
+        interfaceContext.commentBoxes.highlightById(audioObject.id);
     };
     this.stopPlayback = function () {
         // Called when playback has stopped. This gets called even if playback never started!
         window.clearInterval(canvasIntervalID);
         this.canvas.clearCanvas(this.canvas.layer2);
         this.playButton.DOM.textContent = "Play";
+        var box = interfaceContext.commentBoxes.boxes.find(function (a) {
+            return a.id === audioObject.id;
+        });
+        if (box) {
+            box.highlight(false);
+        }
     };
     this.getValue = function () {
         // Return the current value of the object. If there is no value, return 0
@@ -474,7 +481,7 @@
         interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!');
         return;
     }
-    var checks = testState.currentStateMap.interfaces[0],
+    var checks = testState.currentStateMap.interfaces[0].options,
         canContinue = true;
     for (var i = 0; i < checks.length; i++) {
         var checkState = true;
--- a/js/core.js	Fri Apr 14 17:21:11 2017 +0100
+++ b/js/core.js	Sat Apr 15 12:20:51 2017 +0100
@@ -175,7 +175,7 @@
                     gReturnURL = value;
                     break;
                 case "saveFilenamePrefix":
-                    gSaveFilenamePrefix = value;
+                    storage.filenamePrefix = value;
                     break;
             }
         }
@@ -408,51 +408,22 @@
         popup.popupContent.innerHTML = "<span>Please save the file below to give to your test supervisor</span><br>";
         popup.popupContent.appendChild(a);
     } else {
-        var saveUrlSuffix = "";
-        var saveFilenamePrefix = specification.saveFilenamePrefix;
-        if (typeof (saveFilenamePrefix) === "string" && saveFilenamePrefix.length > 0) {
-            saveUrlSuffix = "&saveFilenamePrefix=" + saveFilenamePrefix;
-        }
         var projectReturn = "";
         if (typeof specification.projectReturn == "string") {
             if (specification.projectReturn.substr(0, 4) == "http") {
                 projectReturn = specification.projectReturn;
             }
         }
-        var saveURL = projectReturn + "php/save.php?key=" + storage.SessionKey.key + saveUrlSuffix;
-        var xmlhttp = new XMLHttpRequest();
-        xmlhttp.open("POST", saveURL, true);
-        xmlhttp.setRequestHeader('Content-Type', 'text/xml');
-        xmlhttp.onerror = function () {
-            console.log('Error saving file to server! Presenting download locally');
+        storage.SessionKey.finish().then(function (resolved) {
+            if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) {
+                window.location = specification.returnURL;
+            } else {
+                popup.popupContent.textContent = specification.exitText;
+            }
+        }, function (message) {
+            console.log("Save: Error! " + message.textContent);
             createProjectSave("local");
-        };
-        xmlhttp.onload = function () {
-            console.log(xmlhttp);
-            if (this.status >= 300) {
-                console.log("WARNING - Could not update at this time");
-                createProjectSave("local");
-            } else {
-                var parser = new DOMParser();
-                var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
-                var response = xmlDoc.getElementsByTagName('response')[0];
-                if (response.getAttribute("state") == "OK") {
-                    window.onbeforeunload = undefined;
-                    var file = response.getElementsByTagName("file")[0];
-                    console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
-                    if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) {
-                        window.location = specification.returnURL;
-                    } else {
-                        popup.popupContent.textContent = specification.exitText;
-                    }
-                } else {
-                    var message = response.getElementsByTagName("message");
-                    console.log("Save: Error! " + message.textContent);
-                    createProjectSave("local");
-                }
-            }
-        };
-        xmlhttp.send(file);
+        });
         popup.showPopup();
         popup.popupContent.innerHTML = null;
         popup.popupContent.textContent = "Submitting. Please Wait";
@@ -1213,7 +1184,7 @@
 
 function stateMachine() {
     // Object prototype for tracking and managing the test state
-    
+
     function pickSubPool(pool, numElements) {
         // Assumes each element of pool has function "alwaysInclude"
 
@@ -1227,7 +1198,7 @@
 
         return picked.concat(randomSubArray(pool, numElements - picked.length));
     }
-    
+
     this.stateMap = [];
     this.preTestSurvey = null;
     this.postTestSurvey = null;
@@ -1239,7 +1210,7 @@
 
         // Get the data from Specification
         var pagePool = [];
-        specification.pages.forEach(function(page){
+        specification.pages.forEach(function (page) {
             if (page.position !== null || page.alwaysInclude) {
                 page.alwaysInclude = true;
             }
@@ -1252,7 +1223,7 @@
 
         // Now get the order of pages
         var fixed = [];
-        pagePool.forEach(function(page){
+        pagePool.forEach(function (page) {
             if (page.position !== null) {
                 fixed.push(page);
                 var i = pagePool.indexOf(page);
@@ -1265,7 +1236,7 @@
         }
 
         // Place in the correct order
-        fixed.forEach(function(page) {
+        fixed.forEach(function (page) {
             pagePool.splice(page.position, 0, page);
         });
 
@@ -2483,6 +2454,13 @@
                 this.trackCommentBox.style.width = boxwidth - 6 + "px";
             };
             this.resize();
+            this.highlight = function (state) {
+                if (state === true) {
+                    $(this.trackComment).addClass("comment-box-playing");
+                } else {
+                    $(this.trackComment).removeClass("comment-box-playing");
+                }
+            };
         };
         commentBoxes.createCommentBox = function (audioObject) {
             var node = new this.elementCommentBox(audioObject);
@@ -2515,6 +2493,19 @@
             }
             this.boxes = [];
         };
+        commentBoxes.highlightById = function (id) {
+            if (id === undefined || typeof id !== "number" || id >= this.boxes.length) {
+                console.log("Error - Invalid id");
+                id = -1;
+            }
+            this.boxes.forEach(function (a) {
+                if (a.id === id) {
+                    a.highlight(true);
+                } else {
+                    a.highlight(false);
+                }
+            });
+        };
         return commentBoxes;
     })();
 
@@ -2585,19 +2576,19 @@
         for (var i = 0; i < optCount; i++) {
             var div = document.createElement('div');
             div.className = "comment-checkbox-inputs-flex";
-            
+
             var span = document.createElement('span');
             span.textContent = commentQuestion.options[i].text;
             span.className = 'comment-radio-span';
             div.appendChild(span);
-            
+
             var input = document.createElement('input');
             input.type = 'radio';
             input.name = commentQuestion.id;
             input.setAttribute('setvalue', commentQuestion.options[i].name);
             input.className = 'comment-radio';
             div.appendChild(input);
-            
+
             this.inputs.appendChild(div);
             this.options.push(input);
         }
@@ -2660,19 +2651,19 @@
         for (var i = 0; i < optCount; i++) {
             var div = document.createElement('div');
             div.className = "comment-checkbox-inputs-flex";
-            
+
             var span = document.createElement('span');
             span.textContent = commentQuestion.options[i].text;
             span.className = 'comment-radio-span';
             div.appendChild(span);
-            
+
             var input = document.createElement('input');
             input.type = 'checkbox';
             input.name = commentQuestion.id;
             input.setAttribute('setvalue', commentQuestion.options[i].name);
             input.className = 'comment-radio';
             div.appendChild(input);
-            
+
             this.inputs.appendChild(div);
             this.options.push(input);
         }
@@ -2840,7 +2831,7 @@
     };
 
     this.playhead = (function () {
-        var playhead ={};
+        var playhead = {};
         playhead.object = document.createElement('div');
         playhead.object.className = 'playhead';
         playhead.object.align = 'left';
@@ -3402,6 +3393,7 @@
     this.document = null;
     this.root = null;
     this.state = 0;
+    var pFilenamePrefix = "";
 
     this.initialise = function (existingStore) {
         if (existingStore === undefined) {
@@ -3480,7 +3472,7 @@
                     returnURL = specification.projectReturn;
                 }
             }
-            xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key);
+            xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix);
             xmlhttp.setRequestHeader('Content-Type', 'text/xml');
             xmlhttp.onerror = function () {
                 console.log('Error updating file to server!');
@@ -3505,6 +3497,42 @@
                 }
             };
             xmlhttp.send([hold.innerHTML]);
+        },
+        finish: function () {
+            // Final upload to complete the test
+            this.parent.finish();
+            var hold = document.createElement("div");
+            var clone = this.parent.root.cloneNode(true);
+            hold.appendChild(clone);
+            return new Promise(function (resolve, reject) {
+                var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix;
+                var xmlhttp = new XMLHttpRequest();
+                xmlhttp.open("POST", saveURL);
+                xmlhttp.setRequestHeader('Content-Type', 'text/xml');
+                xmlhttp.onerror = function () {
+                    console.log('Error updating file to server!');
+                    createProjectSave("local");
+                };
+                xmlhttp.onload = function () {
+                    if (this.status >= 300) {
+                        console.log("WARNING - Could not update at this time");
+                        createProjectSave("local");
+                    } else {
+                        var parser = new DOMParser();
+                        var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
+                        var response = xmlDoc.getElementsByTagName('response')[0];
+                        if (response.getAttribute("state") == "OK") {
+                            var file = response.getElementsByTagName("file")[0];
+                            console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
+                            resolve(response);
+                        } else {
+                            var message = response.getElementsByTagName("message");
+                            reject(message);
+                        }
+                    }
+                };
+                xmlhttp.send([hold.innerHTML]);
+            });
         }
     };
 
@@ -3643,13 +3671,25 @@
         this.SessionKey.update();
     };
     this.finish = function () {
-        if (this.state === 0) {
-            this.update();
-        }
         this.state = 1;
         this.root.setAttribute("state", "complete");
         return this.root;
     };
+
+    Object.defineProperties(this, {
+        'filenamePrefix': {
+            'get': function () {
+                return pFilenamePrefix;
+            },
+            'set': function (value) {
+                if (typeof value !== "string") {
+                    value = String(value);
+                }
+                pFilenamePrefix = value;
+                return value;
+            }
+        }
+    });
 }
 
 var window_depedancy_callback;
--- a/python/pythonServer.py	Fri Apr 14 17:21:11 2017 +0100
+++ b/python/pythonServer.py	Sat Apr 15 12:20:51 2017 +0100
@@ -137,16 +137,27 @@
     global curFileName
     global curSaveIndex
     options = self.path.rsplit('?')
-    options = options[1].rsplit('=')
-    key = options[1]
+    options = options[1].rsplit('&')
+    for option in options:
+        optionPair = option.rsplit('=')
+        if optionPair[0] == "key":
+            key = optionPair[1]
+        elif optionPair[0] == "saveFilenamePrefix":
+            prefix = optionPair[1]
+    if key == None:
+        self.send_response(404)
+        return
+    if prefix == None:
+        prefix = "save"
     varLen = int(self.headers['Content-Length'])
     postVars = self.rfile.read(varLen)
     print("Saving file key "+key)
-    file = open('../saves/save-'+key+'.xml','wb')
+    filename = prefix+'-'+key+'.xml'
+    file = open('../saves/'+filename,'wb')
     file.write(postVars)
     file.close()
     try:
-        wbytes = os.path.getsize('../saves/save-'+key+'.xml')
+        wbytes = os.path.getsize('../saves/'+filename)
     except OSError:
         self.send_response(200)
         self.send_header("Content-type", "text/xml")
@@ -155,7 +166,7 @@
     self.send_response(200)
     self.send_header("Content-type", "text/xml")
     self.end_headers()
-    reply = '<response state="OK"><message>OK</message><file bytes="'+str(wbytes)+'">"saves/'+curFileName+'"</file></response>'
+    reply = '<response state="OK"><message>OK</message><file bytes="'+str(wbytes)+'">"saves/'+filename+'"</file></response>'
     if sys.version_info[0] == 2:
         self.wfile.write(reply)
     elif sys.version_info[0] == 3: