diff --git a/cloud.js b/cloud.js
index 28b2a6c5..dfb359b8 100644
--- a/cloud.js
+++ b/cloud.js
@@ -1,658 +1,504 @@
-/*
-
- cloud.js
-
- a backend API for SNAP!
-
- written by Jens Mönig
-
- Copyright (C) 2015 by Jens Mönig
-
- This file is part of Snap!.
-
- Snap! is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of
- the License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-*/
-
-// Global settings /////////////////////////////////////////////////////
-
-/*global modules, IDE_Morph, SnapSerializer, hex_sha512, alert, nop,
-localize*/
-
-modules.cloud = '2015-December-15';
-
-// Global stuff
-
-var Cloud;
-var SnapCloud = new Cloud(
- 'https://snap.apps.miosoft.com/SnapCloud'
-);
-
-// Cloud /////////////////////////////////////////////////////////////
-
-function Cloud(url) {
- this.username = null;
- this.password = null; // hex_sha512 hashed
- this.url = url;
- this.session = null;
- this.limo = null;
- this.route = null;
- this.api = {};
-}
-
-Cloud.prototype.clear = function () {
- this.username = null;
- this.password = null;
- this.session = null;
- this.limo = null;
- this.route = null;
- this.api = {};
-};
-
-Cloud.prototype.hasProtocol = function () {
- return this.url.toLowerCase().indexOf('http') === 0;
-};
-
-Cloud.prototype.setRoute = function (username) {
- var routes = 20,
- userNum = 0,
- i;
-
- for (i = 0; i < username.length; i += 1) {
- userNum += username.charCodeAt(i);
- }
- userNum = userNum % routes + 1;
- this.route = '.sc1m' +
- (userNum < 10 ? '0' : '') +
- userNum;
-};
-
-// Cloud: Snap! API
-
-Cloud.prototype.signup = function (
- username,
- email,
- callBack,
- errorCall
-) {
- // both callBack and errorCall are two-argument functions
- var request = new XMLHttpRequest(),
- myself = this;
- try {
- request.open(
- "GET",
- (this.hasProtocol() ? '' : 'http://')
- + this.url + 'SignUp'
- + '?Username='
- + encodeURIComponent(username)
- + '&Email='
- + encodeURIComponent(email),
- true
- );
- request.setRequestHeader(
- "Content-Type",
- "application/x-www-form-urlencoded"
- );
- request.withCredentials = true;
- request.onreadystatechange = function () {
- if (request.readyState === 4) {
- if (request.responseText) {
- if (request.responseText.indexOf('ERROR') === 0) {
- errorCall.call(
- this,
- request.responseText,
- 'Signup'
- );
- } else {
- callBack.call(
- null,
- request.responseText,
- 'Signup'
- );
- }
- } else {
- errorCall.call(
- null,
- myself.url + 'SignUp',
- localize('could not connect to:')
- );
- }
- }
- };
- request.send(null);
- } catch (err) {
- errorCall.call(this, err.toString(), 'Snap!Cloud');
- }
-};
-
-Cloud.prototype.getPublicProject = function (
- id,
- callBack,
- errorCall
-) {
- // id is Username=username&projectName=projectname,
- // where the values are url-component encoded
- // callBack is a single argument function, errorCall take two args
- var request = new XMLHttpRequest(),
- myself = this;
- try {
- request.open(
- "GET",
- (this.hasProtocol() ? '' : 'http://')
- + this.url + 'RawPublic'
- + '?'
- + id,
- true
- );
- request.setRequestHeader(
- "Content-Type",
- "application/x-www-form-urlencoded"
- );
- request.withCredentials = true;
- request.onreadystatechange = function () {
- if (request.readyState === 4) {
- if (request.responseText) {
- if (request.responseText.indexOf('ERROR') === 0) {
- errorCall.call(
- this,
- request.responseText
- );
- } else {
- callBack.call(
- null,
- request.responseText
- );
- }
- } else {
- errorCall.call(
- null,
- myself.url + 'Public',
- localize('could not connect to:')
- );
- }
- }
- };
- request.send(null);
- } catch (err) {
- errorCall.call(this, err.toString(), 'Snap!Cloud');
- }
-};
-
-Cloud.prototype.resetPassword = function (
- username,
- callBack,
- errorCall
-) {
- // both callBack and errorCall are two-argument functions
- var request = new XMLHttpRequest(),
- myself = this;
- try {
- request.open(
- "GET",
- (this.hasProtocol() ? '' : 'http://')
- + this.url + 'ResetPW'
- + '?Username='
- + encodeURIComponent(username),
- true
- );
- request.setRequestHeader(
- "Content-Type",
- "application/x-www-form-urlencoded"
- );
- request.withCredentials = true;
- request.onreadystatechange = function () {
- if (request.readyState === 4) {
- if (request.responseText) {
- if (request.responseText.indexOf('ERROR') === 0) {
- errorCall.call(
- this,
- request.responseText,
- 'Reset Password'
- );
- } else {
- callBack.call(
- null,
- request.responseText,
- 'Reset Password'
- );
- }
- } else {
- errorCall.call(
- null,
- myself.url + 'ResetPW',
- localize('could not connect to:')
- );
- }
- }
- };
- request.send(null);
- } catch (err) {
- errorCall.call(this, err.toString(), 'Snap!Cloud');
- }
-};
-
-Cloud.prototype.login = function (
- username,
- password,
- callBack,
- errorCall
-) {
- // both callBack and errorCall are two-argument functions
- var request = new XMLHttpRequest(),
- usr = JSON.stringify({'__h': password, '__u': username}),
- myself = this;
- this.setRoute(username);
- try {
- request.open(
- "POST",
- (this.hasProtocol() ? '' : 'http://') +
- this.url +
- '?SESSIONGLUE=' +
- this.route,
- true
- );
- request.setRequestHeader(
- "Content-Type",
- "application/json; charset=utf-8"
- );
- // glue this session to a route:
- request.setRequestHeader('SESSIONGLUE', this.route);
- request.withCredentials = true;
- request.onreadystatechange = function () {
- if (request.readyState === 4) {
- if (request.responseText) {
- myself.api = myself.parseAPI(request.responseText);
- myself.session = request.getResponseHeader('MioCracker')
- .split(';')[0];
- // set the cookie identifier:
- myself.limo = this.getResponseHeader("miocracker")
- .substring(
- 9,
- this.getResponseHeader("miocracker").indexOf("=")
- );
- if (myself.api.logout) {
- myself.username = username;
- myself.password = password;
- callBack.call(null, myself.api, 'Snap!Cloud');
- } else {
- errorCall.call(
- null,
- request.responseText,
- 'connection failed'
- );
- }
- } else {
- errorCall.call(
- null,
- myself.url,
- localize('could not connect to:')
- );
- }
- }
- };
- request.send(usr);
- } catch (err) {
- errorCall.call(this, err.toString(), 'Snap!Cloud');
- }
-};
-
-Cloud.prototype.reconnect = function (
- callBack,
- errorCall
-) {
- if (!(this.username && this.password)) {
- this.message('You are not logged in');
- return;
- }
- this.login(
- this.username,
- this.password,
- callBack,
- errorCall
- );
-};
-
-Cloud.prototype.saveProject = function (ide, callBack, errorCall) {
- var myself = this,
- pdata,
- media,
- size,
- mediaSize;
-
- ide.serializer.isCollectingMedia = true;
- pdata = ide.serializer.serialize(ide.stage);
- media = ide.hasChangedMedia ?
- ide.serializer.mediaXML(ide.projectName) : null;
- ide.serializer.isCollectingMedia = false;
- ide.serializer.flushMedia();
-
- mediaSize = media ? media.length : 0;
- size = pdata.length + mediaSize;
- if (mediaSize > 10485760) {
- new DialogBoxMorph().inform(
- 'Snap!Cloud - Cannot Save Project',
- 'The media inside this project exceeds 10 MB.\n' +
- 'Please reduce the size of costumes or sounds.\n',
- ide.world(),
- ide.cloudIcon(null, new Color(180, 0, 0))
- );
- throw new Error('Project media exceeds 10 MB size limit');
- }
-
- // check if serialized data can be parsed back again
- try {
- ide.serializer.parse(pdata);
- } catch (err) {
- ide.showMessage('Serialization of program data failed:\n' + err);
- throw new Error('Serialization of program data failed:\n' + err);
- }
- if (media !== null) {
- try {
- ide.serializer.parse(media);
- } catch (err) {
- ide.showMessage('Serialization of media failed:\n' + err);
- throw new Error('Serialization of media failed:\n' + err);
- }
- }
- ide.serializer.isCollectingMedia = false;
- ide.serializer.flushMedia();
-
- ide.showMessage('Uploading ' + Math.round(size / 1024) + ' KB...');
- myself.reconnect(
- function () {
- myself.callService(
- 'saveProject',
- function (response, url) {
- callBack.call(null, response, url);
- myself.disconnect();
- ide.hasChangedMedia = false;
- },
- errorCall,
- [
- ide.projectName,
- pdata,
- media,
- pdata.length,
- media ? media.length : 0
- ]
- );
- },
- errorCall
- );
-};
-
-Cloud.prototype.getProjectList = function (callBack, errorCall) {
- var myself = this;
- this.reconnect(
- function () {
- myself.callService(
- 'getProjectList',
- function (response, url) {
- callBack.call(null, response, url);
- myself.disconnect();
- },
- errorCall
- );
- },
- errorCall
- );
-};
-
-Cloud.prototype.changePassword = function (
- oldPW,
- newPW,
- callBack,
- errorCall
-) {
- var myself = this;
- this.reconnect(
- function () {
- myself.callService(
- 'changePassword',
- function (response, url) {
- callBack.call(null, response, url);
- myself.disconnect();
- },
- errorCall,
- [hex_sha512(oldPW), hex_sha512(newPW)]
- );
- },
- errorCall
- );
-};
-
-Cloud.prototype.logout = function (callBack, errorCall) {
- this.clear();
- this.callService(
- 'logout',
- callBack,
- errorCall
- );
-};
-
-Cloud.prototype.disconnect = function () {
- this.callService(
- 'logout',
- nop,
- nop
- );
-};
-
-// Cloud: backend communication
-
-Cloud.prototype.callURL = function (url, callBack, errorCall) {
- // both callBack and errorCall are optional two-argument functions
- var request = new XMLHttpRequest(),
- stickyUrl,
- myself = this;
- try {
- // set the Limo. Also set the glue as a query paramter for backup.
- stickyUrl = url +
- '&SESSIONGLUE=' +
- this.route +
- '&_Limo=' +
- this.limo;
- request.open('GET', stickyUrl, true);
- request.withCredentials = true;
- request.setRequestHeader(
- "Content-Type",
- "application/x-www-form-urlencoded"
- );
- request.setRequestHeader('MioCracker', this.session);
- // Set the glue as a request header.
- request.setRequestHeader('SESSIONGLUE', this.route);
- request.onreadystatechange = function () {
- if (request.readyState === 4) {
- if (request.responseText) {
- var responseList = myself.parseResponse(
- request.responseText
- );
- callBack.call(null, responseList, url);
- } else {
- errorCall.call(
- null,
- url,
- 'no response from:'
- );
- }
- }
- };
- request.send(null);
- } catch (err) {
- errorCall.call(this, err.toString(), url);
- }
-};
-
-Cloud.prototype.callService = function (
- serviceName,
- callBack,
- errorCall,
- args
-) {
- // both callBack and errorCall are optional two-argument functions
- var request = new XMLHttpRequest(),
- service = this.api[serviceName],
- myself = this,
- stickyUrl,
- postDict;
-
- if (!this.session) {
- errorCall.call(null, 'You are not connected', 'Cloud');
- return;
- }
- if (!service) {
- errorCall.call(
- null,
- 'service ' + serviceName + ' is not available',
- 'API'
- );
- return;
- }
- if (args && args.length > 0) {
- postDict = {};
- service.parameters.forEach(function (parm, idx) {
- postDict[parm] = args[idx];
- });
- }
- try {
- stickyUrl = this.url +
- '/' +
- service.url +
- '&SESSIONGLUE=' +
- this.route +
- '&_Limo=' +
- this.limo;
- request.open(service.method, stickyUrl, true);
- request.withCredentials = true;
- request.setRequestHeader(
- "Content-Type",
- "application/x-www-form-urlencoded"
- );
- request.setRequestHeader('MioCracker', this.session);
- request.setRequestHeader('SESSIONGLUE', this.route);
- request.onreadystatechange = function () {
- if (request.readyState === 4) {
- var responseList = [];
- if (request.responseText &&
- request.responseText.indexOf('ERROR') === 0) {
- errorCall.call(
- this,
- request.responseText,
- localize('Service:') + ' ' + localize(serviceName)
- );
- return;
- }
- if (serviceName === 'login') {
- myself.api = myself.parseAPI(request.responseText);
- }
- if (serviceName === 'getRawProject') {
- responseList = request.responseText;
- } else {
- responseList = myself.parseResponse(
- request.responseText
- );
- }
- callBack.call(null, responseList, service.url);
- }
- };
- request.send(this.encodeDict(postDict));
- } catch (err) {
- errorCall.call(this, err.toString(), service.url);
- }
-};
-
-// Cloud: payload transformation
-
-Cloud.prototype.parseAPI = function (src) {
- var api = {},
- services;
- services = src.split(" ");
- services.forEach(function (service) {
- var entries = service.split("&"),
- serviceDescription = {},
- parms;
- entries.forEach(function (entry) {
- var pair = entry.split("="),
- key = decodeURIComponent(pair[0]).toLowerCase(),
- val = decodeURIComponent(pair[1]);
- if (key === "service") {
- api[val] = serviceDescription;
- } else if (key === "parameters") {
- parms = val.split(",");
- if (!(parms.length === 1 && !parms[0])) {
- serviceDescription.parameters = parms;
- }
- } else {
- serviceDescription[key] = val;
- }
- });
- });
- return api;
-};
-
-Cloud.prototype.parseResponse = function (src) {
- var ans = [],
- lines;
- if (!src) {return ans; }
- lines = src.split(" ");
- lines.forEach(function (service) {
- var entries = service.split("&"),
- dict = {};
- entries.forEach(function (entry) {
- var pair = entry.split("="),
- key = decodeURIComponent(pair[0]),
- val = decodeURIComponent(pair[1]);
- dict[key] = val;
- });
- ans.push(dict);
- });
- return ans;
-};
-
-Cloud.prototype.parseDict = function (src) {
- var dict = {};
- if (!src) {return dict; }
- src.split("&").forEach(function (entry) {
- var pair = entry.split("="),
- key = decodeURIComponent(pair[0]),
- val = decodeURIComponent(pair[1]);
- dict[key] = val;
- });
- return dict;
-};
-
-Cloud.prototype.encodeDict = function (dict) {
- var str = '',
- pair,
- key;
- if (!dict) {return null; }
- for (key in dict) {
- if (dict.hasOwnProperty(key)) {
- pair = encodeURIComponent(key)
- + '='
- + encodeURIComponent(dict[key]);
- if (str.length > 0) {
- str += '&';
- }
- str += pair;
- }
- }
- return str;
-};
-
-// Cloud: user messages (to be overridden)
-
-Cloud.prototype.message = function (string) {
- alert(string);
-};
+/*
+
+ cloud.js
+
+ a backend API for SNAP!
+
+ written by Bernat Romagosa
+ inspired in the old cloud API by Jens Mönig
+
+ Copyright (C) 2017 by Bernat Romagosa
+ Copyright (C) 2015 by Jens Mönig
+
+ This file is part of Snap!.
+
+ Snap! is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+*/
+
+// Global settings /////////////////////////////////////////////////////
+
+/*global modules, IDE_Morph, SnapSerializer, nop, localize*/
+
+modules.cloud = '2015-December-15';
+
+// Global stuff
+
+var Cloud;
+
+// Cloud /////////////////////////////////////////////////////////////
+
+function Cloud(url) {
+ this.init(url);
+};
+
+Cloud.prototype.init = function (url) {
+ this.url = url;
+ this.username = null;
+};
+
+
+// Dictionary handling
+
+Cloud.prototype.parseDict = function (src) {
+ var dict = {};
+ if (!src) {return dict; }
+ src.split("&").forEach(function (entry) {
+ var pair = entry.split("="),
+ key = decodeURIComponent(pair[0]),
+ val = decodeURIComponent(pair[1]);
+ dict[key] = val;
+ });
+ return dict;
+};
+
+Cloud.prototype.encodeDict = function (dict) {
+ var str = '',
+ pair,
+ key;
+ if (!dict) {return null; }
+ for (key in dict) {
+ if (dict.hasOwnProperty(key)) {
+ pair = encodeURIComponent(key)
+ + '='
+ + encodeURIComponent(dict[key]);
+ if (str.length > 0) {
+ str += '&';
+ }
+ str += pair;
+ }
+ }
+ return str;
+};
+
+// Error handling
+
+Cloud.genericErrorMessage =
+ 'There was an error while trying to access\n' +
+ 'a Snap!Cloud service. Please try again later.';
+
+Cloud.prototype.genericError = function () {
+ throw new Error(Cloud.genericErrorMessage);
+};
+
+
+// Low level functionality
+
+Cloud.prototype.request = function (
+ method,
+ path,
+ onSuccess,
+ onError,
+ errorMsg,
+ wantsRawResponse,
+ body) {
+
+ var request = new XMLHttpRequest(),
+ myself = this;
+
+ try {
+ request.open(
+ method,
+ this.url + path,
+ true
+ );
+ request.setRequestHeader(
+ 'Content-Type',
+ 'application/json; charset=utf-8'
+ );
+ request.withCredentials = true;
+ request.onreadystatechange = function () {
+ if (request.readyState === 4) {
+ if (request.responseText) {
+ var response =
+ (!wantsRawResponse ||
+ (request.responseText.indexOf('{"errors"') === 0)) ?
+ JSON.parse(request.responseText) :
+ request.responseText;
+
+ if (response.errors) {
+ onError.call(
+ null,
+ response.errors[0],
+ errorMsg
+ );
+ } else {
+ if (onSuccess) {
+ onSuccess.call(null, response.message || response);
+ }
+ }
+ } else {
+ if (onError) {
+ onError.call(
+ null,
+ errorMsg || Cloud.genericErrorMessage,
+ myself.url
+ );
+ } else {
+ myself.genericError();
+ }
+ }
+ }
+ };
+ request.send(body);
+ } catch (err) {
+ onError.call(this, err.toString(), 'Cloud Error');
+ }
+};
+
+Cloud.prototype.withCredentialsRequest = function (
+ method,
+ path,
+ onSuccess,
+ onError,
+ errorMsg,
+ wantsRawResponse,
+ body) {
+
+ var myself = this;
+ this.checkCredentials(
+ function (username) {
+ if (username) {
+ myself.request(
+ method,
+ // %username is replaced by the actual username
+ path.replace('%username', username),
+ onSuccess,
+ onError,
+ errorMsg,
+ wantsRawResponse,
+ body);
+ } else {
+ onError.call(this, 'You are not logged in', 'Snap!Cloud');
+ }
+ }
+ );
+};
+
+
+// Credentials management
+
+Cloud.prototype.initSession = function (onSuccess) {
+ var myself = this;
+ this.request(
+ 'POST',
+ '/init',
+ function () { myself.checkCredentials(onSuccess); },
+ nop,
+ null,
+ true
+ );
+};
+
+Cloud.prototype.checkCredentials = function (onSuccess, onError) {
+ var myself = this;
+ this.getCurrentUser(
+ function (user) {
+ if (user.username) {
+ myself.username = user.username;
+ }
+ if (onSuccess) { onSuccess.call(null, user.username, user.isadmin); }
+ },
+ onError
+ );
+};
+
+Cloud.prototype.getCurrentUser = function (onSuccess, onError) {
+ this.request('GET', '/users/c', onSuccess, onError, 'Could not retrieve current user');
+};
+
+Cloud.prototype.getUser = function (username, onSuccess, onError) {
+ this.request('GET', '/users/' + username, onSuccess, onError, 'Could not retrieve user');
+};
+
+Cloud.prototype.logout = function (onSuccess, onError) {
+ this.username = null;
+ this.request(
+ 'POST',
+ '/logout',
+ onSuccess,
+ onError,
+ 'logout failed'
+ );
+};
+
+Cloud.prototype.login = function (username, password, persist, onSuccess, onError) {
+ var myself = this;
+ this.request(
+ 'POST',
+ '/users/' + username + '/login?' +
+ this.encodeDict({
+ persist: persist
+ }),
+ function () {
+ myself.checkCredentials(onSuccess, onError);
+ },
+ onError,
+ 'login failed',
+ 'false', // wants raw response
+ hex_sha512(password) // password travels inside the body
+ );
+};
+
+Cloud.prototype.signup = function (username, password, passwordRepeat, email, onSuccess, onError) {
+ this.request(
+ 'POST',
+ '/users/' + username + '?' + this.encodeDict({
+ email: email,
+ password: hex_sha512(password),
+ password_repeat: hex_sha512(passwordRepeat)
+ }),
+ onSuccess,
+ onError,
+ 'signup failed');
+};
+
+Cloud.prototype.changePassword = function (password, newPassword, passwordRepeat, onSuccess, onError) {
+ this.withCredentialsRequest(
+ 'POST',
+ '/users/%username/newpassword?' + this.encodeDict({
+ oldpassword: hex_sha512(password),
+ password_repeat: hex_sha512(passwordRepeat),
+ newpassword: hex_sha512(newPassword)
+ }),
+ onSuccess,
+ onError,
+ 'Could not change password'
+ );
+
+};
+
+// Projects
+
+Cloud.prototype.saveProject = function (ide, onSuccess, onError) {
+ var myself = this;
+ this.checkCredentials(
+ function (username) {
+ if (username) {
+ var xml = ide.serializer.serialize(ide.stage),
+ thumbnail = ide.stage.thumbnail(
+ SnapSerializer.prototype.thumbnailSize).toDataURL(),
+ body, mediaSize, size;
+
+ ide.serializer.isCollectingMedia = true;
+ body = {
+ notes: ide.projectNotes,
+ xml: xml,
+ media: ide.hasChangedMedia ?
+ ide.serializer.mediaXML(ide.projectName) : null,
+ thumbnail: thumbnail
+ };
+ ide.serializer.isCollectingMedia = false;
+ ide.serializer.flushMedia();
+
+ mediaSize = body.media ? body.media.length : 0;
+ size = body.xml.length + mediaSize;
+ if (mediaSize > 10485760) {
+ new DialogBoxMorph().inform(
+ 'Snap!Cloud - Cannot Save Project',
+ 'The media inside this project exceeds 10 MB.\n' +
+ 'Please reduce the size of costumes or sounds.\n',
+ ide.world(),
+ ide.cloudIcon(null, new Color(180, 0, 0))
+ );
+ throw new Error('Project media exceeds 10 MB size limit');
+ }
+
+ // check if serialized data can be parsed back again
+ try {
+ ide.serializer.parse(body.xml);
+ } catch (err) {
+ ide.showMessage('Serialization of program data failed:\n' + err);
+ throw new Error('Serialization of program data failed:\n' + err);
+ }
+ if (body.media !== null) {
+ try {
+ ide.serializer.parse(body.media);
+ } catch (err) {
+ ide.showMessage('Serialization of media failed:\n' + err);
+ throw new Error('Serialization of media failed:\n' + err);
+ }
+ }
+ ide.serializer.isCollectingMedia = false;
+ ide.serializer.flushMedia();
+
+ ide.showMessage('Uploading ' + Math.round(size / 1024) + ' KB...');
+
+ myself.request(
+ 'POST',
+ '/projects/' + username + '/' + ide.projectName,
+ onSuccess,
+ onError,
+ 'Project could not be saved',
+ false,
+ JSON.stringify(body), // POST body
+ );
+ } else {
+ onError.call(this, 'You are not logged in', 'Snap!Cloud');
+ }
+ }
+ );
+};
+
+Cloud.prototype.getProjectList = function (onSuccess, onError, withThumbnail) {
+ var path = '/projects/%username';
+
+ if (withThumbnail) {
+ path += '?withthumbnail=true';
+ }
+
+ this.withCredentialsRequest(
+ 'GET',
+ path,
+ onSuccess,
+ onError,
+ 'Could not fetch projects'
+ );
+};
+
+Cloud.prototype.getPublishedProjectList = function (username, page, pageSize, searchTerm, onSuccess, onError, withThumbnail) {
+ var path = '/projects' + (username ? '/' + username : '') + '?ispublished=true';
+
+ if (withThumbnail) {
+ path += '&withthumbnail=true';
+ }
+
+ if (page) {
+ path += '&page=' + page + '&pagesize=' + (pageSize || 16);
+ }
+
+ if (searchTerm) {
+ path += '&matchtext=' + searchTerm;
+ }
+
+ this.request(
+ 'GET',
+ path,
+ onSuccess,
+ onError,
+ 'Could not fetch projects'
+ );
+};
+
+Cloud.prototype.getThumbnail = function (username, projectName, onSuccess, onError) {
+ this[username ? 'request' : 'withCredentialsRequest'](
+ 'GET',
+ '/projects/' + (username || '%username') + '/'
+ + projectName + '/thumbnail',
+ onSuccess,
+ onError,
+ 'Could not fetch thumbnail',
+ true
+ );
+};
+
+Cloud.prototype.getProject = function (projectName, onSuccess, onError) {
+ this.withCredentialsRequest(
+ 'GET',
+ '/projects/%username/' + projectName,
+ onSuccess,
+ onError,
+ 'Could not fetch project ' + projectName,
+ true
+ );
+};
+
+Cloud.prototype.getPublicProject = function (projectName, username, onSuccess, onError) {
+ this.request(
+ 'GET',
+ '/projects/' + username + '/' + projectName,
+ onSuccess,
+ onError,
+ 'Could not fetch project ' + projectName,
+ true
+ );
+};
+
+Cloud.prototype.getProjectMetadata = function (projectName, username, onSuccess, onError) {
+ this.request(
+ 'GET',
+ '/projects/' + username + '/' + projectName + '/metadata',
+ onSuccess,
+ onError,
+ 'Could not fetch metadata for ' + projectName
+ );
+};
+
+Cloud.prototype.deleteProject = function (projectName, username, onSuccess, onError) {
+ this[username ? 'request' : 'withCredentialsRequest'](
+ 'DELETE',
+ '/projects/' + (username || '%username') + '/' + projectName,
+ onSuccess,
+ onError,
+ 'Could not delete project'
+ );
+};
+
+Cloud.prototype.shareProject = function (projectName, username, onSuccess, onError) {
+ this[username ? 'request' : 'withCredentialsRequest'](
+ 'POST',
+ '/projects/' + (username || '%username') + '/'
+ + projectName + '/metadata?ispublic=true',
+ onSuccess,
+ onError,
+ 'Could not share project'
+ );
+};
+
+Cloud.prototype.unshareProject = function (projectName, username, onSuccess, onError) {
+ this[username ? 'request' : 'withCredentialsRequest'](
+ 'POST',
+ '/projects/' + (username || '%username') + '/'
+ + projectName + '/metadata?ispublic=false&ispublished=false',
+ onSuccess,
+ onError,
+ 'Could not unshare project'
+ );
+};
+
+Cloud.prototype.publishProject = function (projectName, username, onSuccess, onError) {
+ this[username ? 'request' : 'withCredentialsRequest'](
+ 'POST',
+ '/projects/' + (username || '%username') + '/'
+ + projectName + '/metadata?ispublished=true',
+ onSuccess,
+ onError,
+ 'Could not publish project'
+ );
+};
+
+Cloud.prototype.unpublishProject = function (projectName, username, onSuccess, onError) {
+ this[username ? 'request' : 'withCredentialsRequest'](
+ 'POST',
+ '/projects/' + (username || '%username') + '/'
+ + projectName + '/metadata?ispublished=false',
+ onSuccess,
+ onError,
+ 'Could not unpublish project'
+ );
+};
+
+Cloud.prototype.updateNotes = function (projectName, notes, onSuccess, onError) {
+ this.withCredentialsRequest(
+ 'POST',
+ '/projects/%username/' + projectName + '/metadata',
+ onSuccess,
+ onError,
+ 'Could not update project notes',
+ false, // wants raw response
+ JSON.stringify({ notes: notes })
+ );
+};
+
+var SnapCloud = new Cloud('https://snap-cloud.cs10.org');
diff --git a/gui.js b/gui.js
index 83e0fd8f..5d9441f9 100644
--- a/gui.js
+++ b/gui.js
@@ -241,6 +241,10 @@ IDE_Morph.prototype.init = function (isAutoFill) {
this.corralBar = null;
this.corral = null;
+ this.embedPlayButton = null;
+ this.embedOverlay = null;
+ this.isEmbedMode = false;
+
this.isAutoFill = isAutoFill === undefined ? true : isAutoFill;
this.isAppMode = false;
this.isSmallStage = false;
@@ -268,20 +272,13 @@ IDE_Morph.prototype.init = function (isAutoFill) {
IDE_Morph.prototype.openIn = function (world) {
var hash, usr, myself = this, urlLanguage = null;
- // get persistent user data, if any
- if (this.hasLocalStorage()) {
- usr = localStorage['-snap-user'];
- if (usr) {
- usr = SnapCloud.parseResponse(usr)[0];
- if (usr) {
- SnapCloud.username = usr.username || null;
- SnapCloud.password = usr.password || null;
- if (SnapCloud.username) {
- this.source = 'cloud';
- }
+ SnapCloud.initSession(
+ function (username) {
+ if (username) {
+ myself.source = 'cloud';
}
}
- }
+ );
this.buildPanes();
world.add(this);
@@ -328,7 +325,10 @@ IDE_Morph.prototype.openIn = function (world) {
}
}
- function applyFlags(dict) {
+ function applyFlags(dict) {
+ if (dict.embedMode) {
+ myself.setEmbedMode();
+ }
if (dict.editMode) {
myself.toggleAppMode(false);
} else {
@@ -344,7 +344,7 @@ IDE_Morph.prototype.openIn = function (world) {
if (dict.noExitWarning) {
window.onbeforeunload = nop;
}
- }
+ }
// dynamic notifications from non-source text files
// has some issues, commented out for now
@@ -405,7 +405,8 @@ IDE_Morph.prototype.openIn = function (world) {
dict.Username = dict.Username.toLowerCase();
SnapCloud.getPublicProject(
- SnapCloud.encodeDict(dict),
+ dict.ProjectName,
+ dict.Username,
function (projectData) {
var msg;
myself.nextSteps([
@@ -442,10 +443,10 @@ IDE_Morph.prototype.openIn = function (world) {
// make sure to lowercase the username
dict = SnapCloud.parseDict(location.hash.substr(7));
- dict.Username = dict.Username.toLowerCase();
SnapCloud.getPublicProject(
- SnapCloud.encodeDict(dict),
+ dict.ProjectName,
+ dict.Username,
function (projectData) {
var msg;
myself.nextSteps([
@@ -478,10 +479,10 @@ IDE_Morph.prototype.openIn = function (world) {
// make sure to lowercase the username
dict = SnapCloud.parseDict(location.hash.substr(4));
- dict.Username = dict.Username.toLowerCase();
SnapCloud.getPublicProject(
- SnapCloud.encodeDict(dict),
+ dict.ProjectName,
+ dict.Username,
function (projectData) {
myself.saveXMLAs(projectData, dict.ProjectName);
myself.showMessage(
@@ -1724,7 +1725,24 @@ IDE_Morph.prototype.fixLayout = function (situation) {
if (situation !== 'refreshPalette') {
// stage
- if (this.isAppMode) {
+ if (this.isEmbedMode) {
+ this.stage.setScale(Math.floor(Math.min(
+ this.width() / this.stage.dimensions.x,
+ this.height() / this.stage.dimensions.y
+ ) * 10) / 10);
+
+ this.embedPlayButton.size = Math.floor(Math.min(
+ this.width(), this.height())) / 3;
+ this.embedPlayButton.setWidth(this.embedPlayButton.size);
+ this.embedPlayButton.setHeight(this.embedPlayButton.size);
+
+ if (this.embedOverlay) {
+ this.embedOverlay.setExtent(this.extent());
+ }
+
+ this.stage.setCenter(this.center());
+ this.embedPlayButton.setCenter(this.center());
+ } else if (this.isAppMode) {
this.stage.setScale(Math.floor(Math.min(
(this.width() - padding * 2) / this.stage.dimensions.x,
(this.height() - this.controlBar.height() * 2 - padding * 2)
@@ -2559,15 +2577,12 @@ IDE_Morph.prototype.cloudMenu = function () {
function () {
myself.prompt('Author name…', function (usr) {
myself.prompt('Project name...', function (prj) {
- var id = 'Username=' +
- encodeURIComponent(usr.toLowerCase()) +
- '&ProjectName=' +
- encodeURIComponent(prj);
myself.showMessage(
'Fetching project\nfrom the cloud...'
);
SnapCloud.getPublicProject(
- id,
+ prj,
+ usr,
function (projectData) {
var msg;
if (!Process.prototype.isCatchingErrors) {
@@ -4659,6 +4674,28 @@ IDE_Morph.prototype.toggleSliderExecute = function () {
!ArgMorph.prototype.executeOnSliderEdit;
};
+IDE_Morph.prototype.setEmbedMode = function () {
+ var myself = this;
+ this.embedOverlay = new Morph();
+ this.embedOverlay.color = new Color(128, 128, 128);
+ this.embedOverlay.alpha = 0.5;
+
+ this.embedPlayButton = new SymbolMorph('pointRight');
+ this.embedPlayButton.color = new Color(128, 255, 128);
+
+ this.embedPlayButton.mouseClickLeft = function () {
+ myself.runScripts();
+ myself.embedOverlay.destroy();
+ this.destroy();
+ };
+
+ this.isEmbedMode = true;
+ this.controlBar.hide();
+ this.add(this.embedOverlay);
+ this.add(this.embedPlayButton);
+ this.fixLayout();
+};
+
IDE_Morph.prototype.toggleAppMode = function (appMode) {
var world = this.world(),
elements = [
@@ -5095,21 +5132,11 @@ IDE_Morph.prototype.initializeCloud = function () {
new DialogBoxMorph(
null,
function (user) {
- var pwh = hex_sha512(user.password),
- str;
SnapCloud.login(
user.username,
- pwh,
+ user.password,
+ user.choice,
function () {
- if (user.choice) {
- str = SnapCloud.encodeDict(
- {
- username: user.username,
- password: pwh
- }
- );
- localStorage['-snap-user'] = str;
- }
myself.source = 'cloud';
myself.showMessage('now connected.', 2);
},
@@ -5133,23 +5160,20 @@ IDE_Morph.prototype.initializeCloud = function () {
IDE_Morph.prototype.createCloudAccount = function () {
var myself = this,
world = this.world();
-/*
- // force-logout, commented out for now:
- delete localStorage['-snap-user'];
- SnapCloud.clear();
-*/
+
new DialogBoxMorph(
null,
function (user) {
SnapCloud.signup(
user.username,
+ user.password,
+ user.passwordRepeat,
user.email,
function (txt, title) {
new DialogBoxMorph().inform(
title,
txt +
- '.\n\nAn e-mail with your password\n' +
- 'has been sent to the address provided',
+ '.\n\nYou can now log in.',
world,
myself.cloudIcon(null, new Color(0, 180, 0))
);
@@ -5174,11 +5198,7 @@ IDE_Morph.prototype.createCloudAccount = function () {
IDE_Morph.prototype.resetCloudPassword = function () {
var myself = this,
world = this.world();
-/*
- // force-logout, commented out for now:
- delete localStorage['-snap-user'];
- SnapCloud.clear();
-*/
+
new DialogBoxMorph(
null,
function (user) {
@@ -5221,8 +5241,8 @@ IDE_Morph.prototype.changeCloudPassword = function () {
SnapCloud.changePassword(
user.oldpassword,
user.password,
+ user.passwordRepeat,
function () {
- myself.logout();
myself.showMessage('password has been changed.', 2);
},
myself.cloudError()
@@ -5244,14 +5264,11 @@ IDE_Morph.prototype.changeCloudPassword = function () {
IDE_Morph.prototype.logout = function () {
var myself = this;
- delete localStorage['-snap-user'];
SnapCloud.logout(
function () {
- SnapCloud.clear();
myself.showMessage('disconnected.', 2);
},
function () {
- SnapCloud.clear();
myself.showMessage('disconnected.', 2);
}
);
@@ -5735,6 +5752,12 @@ ProjectDialogMorph.prototype.buildContents = function () {
this.unshareButton = this.addButton('unshareProject', 'Unshare');
this.shareButton.hide();
this.unshareButton.hide();
+ /*
+ this.publishButton = this.addButton('publishProject', 'Publish');
+ this.unpublishButton = this.addButton('unpublishProject', 'Unpublish');
+ this.publishButton.hide();
+ this.unpublishButton.hide();
+ */
this.deleteButton = this.addButton('deleteProject', 'Delete');
this.addButton('cancel', 'Cancel');
@@ -5882,16 +5905,8 @@ ProjectDialogMorph.prototype.buildFilterField = function () {
myself.listField.elements =
myself.projectList.filter(function (aProject) {
- var name,
- notes;
-
- if (aProject.ProjectName) { // cloud
- name = aProject.ProjectName;
- notes = aProject.Notes;
- } else { // local or examples
- name = aProject.name;
+ var name = aProject.projectname || aProject.name,
notes = aProject.notes || '';
- }
return name.toLowerCase().indexOf(text.toLowerCase()) > -1 ||
notes.toLowerCase().indexOf(text.toLowerCase()) > -1;
@@ -5925,10 +5940,10 @@ ProjectDialogMorph.prototype.setSource = function (source) {
msg = myself.ide.showMessage('Updating\nproject list...');
this.projectList = [];
SnapCloud.getProjectList(
- function (projectList) {
+ function (response) {
// Don't show cloud projects if user has since switch panes.
if (myself.source === 'cloud') {
- myself.installCloudProjectList(projectList);
+ myself.installCloudProjectList(response.projects);
}
msg.destroy();
},
@@ -6019,6 +6034,10 @@ ProjectDialogMorph.prototype.setSource = function (source) {
this.body.add(this.listField);
this.shareButton.hide();
this.unshareButton.hide();
+ /*
+ this.publishButton.hide();
+ this.unpublishButton.hide();
+ */
if (this.source === 'local') {
this.deleteButton.show();
} else { // examples
@@ -6058,9 +6077,9 @@ ProjectDialogMorph.prototype.getExamplesProjectList = function () {
ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
var myself = this;
- this.projectList = pl || [];
+ this.projectList = pl[0] ? pl : [];
this.projectList.sort(function (x, y) {
- return x.ProjectName.toLowerCase() < y.ProjectName.toLowerCase() ?
+ return x.projectname.toLowerCase() < y.projectname.toLowerCase() ?
-1 : 1;
});
@@ -6069,15 +6088,19 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
this.projectList,
this.projectList.length > 0 ?
function (element) {
- return element.ProjectName || element;
+ return element.projectname || element;
} : null,
[ // format: display shared project names bold
[
'bold',
- function (proj) {return proj.Public === 'true'; }
+ function (proj) { return proj.ispublic; }
+ ],
+ [
+ 'italic',
+ function (proj) { return proj.ispublished; }
]
],
- function () {myself.ok(); }
+ function () { myself.ok(); }
);
this.fixListFieldItemColors();
this.listField.fixLayout = nop;
@@ -6091,17 +6114,25 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
this.listField.action = function (item) {
if (item === undefined) {return; }
if (myself.nameField) {
- myself.nameField.setContents(item.ProjectName || '');
+ myself.nameField.setContents(item.projectname || '');
}
if (myself.task === 'open') {
- myself.notesText.text = item.Notes || '';
+ myself.notesText.text = item.notes || '';
myself.notesText.drawNew();
myself.notesField.contents.adjustBounds();
- myself.preview.texture = item.Thumbnail || null;
- myself.preview.cachedTexture = null;
+ myself.preview.texture = '';
myself.preview.drawNew();
+ // we ask for the thumbnail when selecting a project
+ SnapCloud.getThumbnail(
+ null, // username is implicit
+ item.projectname,
+ function (thumbnail) {
+ myself.preview.texture = thumbnail;
+ myself.preview.cachedTexture = null;
+ myself.preview.drawNew();
+ });
(new SpeechBubbleMorph(new TextMorph(
- localize('last changed') + '\n' + item.Updated,
+ localize('last changed') + '\n' + item.lastupdated,
null,
null,
null,
@@ -6112,12 +6143,25 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
myself.preview.rightCenter().add(new Point(2, 0))
);
}
- if (item.Public === 'true') {
+ if (item.ispublic) {
myself.shareButton.hide();
myself.unshareButton.show();
+ /*
+ if (item.ispublished) {
+ myself.publishButton.hide();
+ myself.unpublishButton.show();
+ } else {
+ myself.publishButton.show();
+ myself.unpublishButton.hide();
+ }
+ */
} else {
myself.unshareButton.hide();
myself.shareButton.show();
+ /*
+ myself.publishButton.hide();
+ myself.unpublishButton.hide();
+ */
}
myself.buttons.fixLayout();
myself.fixLayout();
@@ -6175,30 +6219,21 @@ ProjectDialogMorph.prototype.openCloudProject = function (project) {
ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) {
var myself = this;
- SnapCloud.reconnect(
- function () {
- SnapCloud.callService(
- 'getRawProject',
- function (response) {
- SnapCloud.disconnect();
- /*
- if (myself.world().currentKey === 16) {
- myself.ide.download(response);
- return;
- }
- */
- myself.ide.source = 'cloud';
- myself.ide.droppedText(response);
- if (proj.Public === 'true') {
- location.hash = '#present:Username=' +
- encodeURIComponent(SnapCloud.username) +
- '&ProjectName=' +
- encodeURIComponent(proj.ProjectName);
- }
- },
- myself.ide.cloudError(),
- [proj.ProjectName]
- );
+ SnapCloud.getProject(
+ proj.projectname,
+ function (clouddata) {
+ myself.ide.source = 'cloud';
+ myself.ide.nextSteps([
+ function () {
+ myself.ide.openCloudDataString(clouddata);
+ }
+ ]);
+ if (proj.ispublic) {
+ location.hash = '#present:Username=' +
+ encodeURIComponent(SnapCloud.username) +
+ '&ProjectName=' +
+ encodeURIComponent(proj.projectname);
+ }
},
myself.ide.cloudError()
);
@@ -6211,11 +6246,12 @@ ProjectDialogMorph.prototype.saveProject = function () {
myself = this;
this.ide.projectNotes = notes || this.ide.projectNotes;
+
if (name) {
if (this.source === 'cloud') {
if (detect(
this.projectList,
- function (item) {return item.ProjectName === name; }
+ function (item) {return item.projectName === name; }
)) {
this.ide.confirm(
localize(
@@ -6284,26 +6320,20 @@ ProjectDialogMorph.prototype.deleteProject = function () {
this.ide.confirm(
localize(
'Are you sure you want to delete'
- ) + '\n"' + proj.ProjectName + '"?',
+ ) + '\n"' + proj.projectname + '"?',
'Delete Project',
function () {
- SnapCloud.reconnect(
+ SnapCloud.deleteProject(
+ proj.projectname,
+ null, // username is implicit
function () {
- SnapCloud.callService(
- 'deleteProject',
- function () {
- SnapCloud.disconnect();
- myself.ide.hasChangedMedia = true;
- idx = myself.projectList.indexOf(proj);
- myself.projectList.splice(idx, 1);
- myself.installCloudProjectList(
- myself.projectList
- ); // refresh list
- },
- myself.ide.cloudError(),
- [proj.ProjectName]
- );
- },
+ myself.ide.hasChangedMedia = true;
+ idx = myself.projectList.indexOf(proj);
+ myself.projectList.splice(idx, 1);
+ myself.installCloudProjectList(
+ myself.projectList
+ ); // refresh list
+ },
myself.ide.cloudError()
);
}
@@ -6335,37 +6365,36 @@ ProjectDialogMorph.prototype.shareProject = function () {
if (proj) {
this.ide.confirm(
localize(
- 'Are you sure you want to publish'
- ) + '\n"' + proj.ProjectName + '"?',
+ 'Are you sure you want to share'
+ ) + '\n"' + proj.projectname + '"?',
'Share Project',
function () {
myself.ide.showMessage('sharing\nproject...');
- SnapCloud.reconnect(
+ SnapCloud.shareProject(
+ proj.projectname,
+ null, // username is implicit
function () {
- SnapCloud.callService(
- 'publishProject',
- function () {
- SnapCloud.disconnect();
- proj.Public = 'true';
- myself.unshareButton.show();
- myself.shareButton.hide();
- entry.label.isBold = true;
- entry.label.drawNew();
- entry.label.changed();
- myself.buttons.fixLayout();
- myself.drawNew();
- myself.ide.showMessage('shared.', 2);
- },
- myself.ide.cloudError(),
- [proj.ProjectName]
- );
+ proj.ispublic = true;
+ myself.unshareButton.show();
+ myself.shareButton.hide();
+ /*
+ myself.publishButton.show();
+ myself.unpublishButton.hide();
+ */
+ entry.label.isBold = true;
+ entry.label.drawNew();
+ entry.label.changed();
+ myself.buttons.fixLayout();
+ myself.drawNew();
+ myself.ide.showMessage('shared.', 2);
+
// Set the Shared URL if the project is currently open
- if (proj.ProjectName === ide.projectName) {
+ if (proj.projectname === ide.projectName) {
var usr = SnapCloud.username,
projectId = 'Username=' +
encodeURIComponent(usr.toLowerCase()) +
'&ProjectName=' +
- encodeURIComponent(proj.ProjectName);
+ encodeURIComponent(proj.projectname);
location.hash = 'present:' + projectId;
}
},
@@ -6382,38 +6411,119 @@ ProjectDialogMorph.prototype.unshareProject = function () {
proj = this.listField.selected,
entry = this.listField.active;
+ if (proj) {
+ this.ide.confirm(
+ localize(
+ 'Are you sure you want to unshare'
+ ) + '\n"' + proj.projectname + '"?',
+ 'Unshare Project',
+ function () {
+ myself.ide.showMessage('unsharing\nproject...');
+ SnapCloud.unshareProject(
+ proj.projectname,
+ null, // username is implicit
+ function () {
+ proj.ispublic = false;
+ myself.shareButton.show();
+ myself.unshareButton.hide();
+ /*
+ myself.publishButton.hide();
+ myself.unpublishButton.hide();
+ */
+ entry.label.isBold = false;
+ entry.label.isItalic = false;
+ entry.label.drawNew();
+ entry.label.changed();
+ myself.buttons.fixLayout();
+ myself.drawNew();
+ myself.ide.showMessage('unshared.', 2);
+ if (proj.projectname === ide.projectName) {
+ location.hash = '';
+ }
+ },
+ myself.ide.cloudError()
+ );
+ }
+ );
+ }
+};
+
+ProjectDialogMorph.prototype.publishProject = function () {
+ var myself = this,
+ ide = this.ide,
+ proj = this.listField.selected,
+ entry = this.listField.active;
+
+ if (proj) {
+ this.ide.confirm(
+ localize(
+ 'Are you sure you want to publish'
+ ) + '\n"' + proj.projectname + '"?',
+ 'Publish Project',
+ function () {
+ myself.ide.showMessage('publishing\nproject...');
+ SnapCloud.publishProject(
+ proj.projectname,
+ null, // username is implicit
+ function () {
+ proj.ispublished = true;
+ myself.unshareButton.show();
+ myself.shareButton.hide();
+ myself.publishButton.hide();
+ myself.unpublishButton.show();
+ entry.label.isItalic = true;
+ entry.label.drawNew();
+ entry.label.changed();
+ myself.buttons.fixLayout();
+ myself.drawNew();
+ myself.ide.showMessage('published.', 2);
+
+ // Set the Shared URL if the project is currently open
+ if (proj.projectname === ide.projectName) {
+ var usr = SnapCloud.username,
+ projectId = 'Username=' +
+ encodeURIComponent(usr.toLowerCase()) +
+ '&ProjectName=' +
+ encodeURIComponent(proj.projectname);
+ location.hash = 'present:' + projectId;
+ }
+ },
+ myself.ide.cloudError()
+ );
+ }
+ );
+ }
+};
+
+ProjectDialogMorph.prototype.unpublishProject = function () {
+ var myself = this,
+ ide = this.ide,
+ proj = this.listField.selected,
+ entry = this.listField.active;
if (proj) {
this.ide.confirm(
localize(
'Are you sure you want to unpublish'
- ) + '\n"' + proj.ProjectName + '"?',
- 'Unshare Project',
+ ) + '\n"' + proj.projectname + '"?',
+ 'Unpublish Project',
function () {
- myself.ide.showMessage('unsharing\nproject...');
- SnapCloud.reconnect(
+ myself.ide.showMessage('unpublishing\nproject...');
+ SnapCloud.unpublishProject(
+ proj.projectname,
+ null, // username is implicit
function () {
- SnapCloud.callService(
- 'unpublishProject',
- function () {
- SnapCloud.disconnect();
- proj.Public = 'false';
- myself.shareButton.show();
- myself.unshareButton.hide();
- entry.label.isBold = false;
- entry.label.drawNew();
- entry.label.changed();
- myself.buttons.fixLayout();
- myself.drawNew();
- myself.ide.showMessage('unshared.', 2);
- },
- myself.ide.cloudError(),
- [proj.ProjectName]
- );
- // Remove the shared URL if the project is open.
- if (proj.ProjectName === ide.projectName) {
- location.hash = '';
- }
+ proj.ispublished = false;
+ myself.unshareButton.show();
+ myself.shareButton.hide();
+ myself.publishButton.show();
+ myself.unpublishButton.hide();
+ entry.label.isItalic = false;
+ entry.label.drawNew();
+ entry.label.changed();
+ myself.buttons.fixLayout();
+ myself.drawNew();
+ myself.ide.showMessage('unpublished.', 2);
},
myself.ide.cloudError()
);
diff --git a/history.txt b/history.txt
index 15ce0fd3..2297f874 100755
--- a/history.txt
+++ b/history.txt
@@ -1626,7 +1626,7 @@ ______
130415
------
-* Blocks: place sticky comments on World layer on dragging their anchor block
+* Blocks: place sticky comments on World layer on dragging their anchor block
130416
------
@@ -1704,7 +1704,7 @@ ______
130514
------
* paint.js: Paint editor, first version, contributed by Kartik Chandra, Yay!!
-* Threads, Objects, Blocks: Broadcast & message enhancements: When I receive , and getLastMessage reporter + watcher
+* Threads, Objects, Blocks: Broadcast & message enhancements: When I receive , and getLastMessage reporter + watcher
130515
------
@@ -1726,7 +1726,7 @@ ______
130605
------
-* Objects: fix for hiding 'getLastAnswer' and 'getTimer' primitives
+* Objects: fix for hiding 'getLastAnswer' and 'getTimer' primitives
130606
------
@@ -1837,7 +1837,7 @@ ______
130801
------
* Blocks, Threads: "whitespace" & other options in SPLIT reporter's dropdown
-* Blocks: Italicize editable input options (e.g. for the SPLT block)
+* Blocks: Italicize editable input options (e.g. for the SPLT block)
* Blocks: Undrop Reporters feature (in script areas' context menus)
130802
@@ -2245,7 +2245,7 @@ ______
* Objects, GUI: duplicate and clone nested sprites
* GUI, Store: export and import nested sprites
* Objects: double clicking on a sprite in the stage selects it in the IDE
-* Objects: added ‘move’ option to the sprite context menu, lets the user move (nested) sprites in edit mode without changing their layering, and also sprites marked “undraggable”
+* Objects: added ‘move’ option to the sprite context menu, lets the user move (nested) sprites in edit mode without changing their layering, and also sprites marked “undraggable”
* updated Portuguese translation, thanks, Manuel!
* updated German translation
* Morphic: fixed #497 (prevent bubble shadows from getting cut-off)
@@ -2285,7 +2285,7 @@ ______
140930
------
-* Objects: fixed #593 match broadcast numbers with event hat blocks containing strings that can be parsed as numbers
+* Objects: fixed #593 match broadcast numbers with event hat blocks containing strings that can be parsed as numbers
* BYOB: allow percent symbols in custom block texts (fix #361), thanks, @Gubolin!!
* Morphic: allow negative min/max values for sliders (fix #285), thanks, @Gubolin!!
* Objects: fixed #378 (disable context menus for boolean representations)
@@ -2927,7 +2927,7 @@ http://snap.berkeley.edu/run#cloud:Username=jens&ProjectName=rotation
* Updated Simplified Chinese translation, thanks to @ubertao!
* Media import dialog with thumbnail, thanks to @ubertao!
-== v4.0.7.2 ====
+== v4.0.7.2 ====
160714
------
@@ -3050,7 +3050,7 @@ http://snap.berkeley.edu/run#cloud:Username=jens&ProjectName=rotation
160924
------
-* don’t update the recursion cache when updating a custom block definition
+* don’t update the recursion cache when updating a custom block definition
160929
------
@@ -3167,7 +3167,7 @@ http://snap.berkeley.edu/run#cloud:Username=jens&ProjectName=rotation
161206
------
-* GUI: Switch to asynchronous loading of resources (costumes, sounds, libraries etc.)
+* GUI: Switch to asynchronous loading of resources (costumes, sounds, libraries etc.)
* Morphic: Added support for dropping links to SVGs from other web pages onto the World
* GUI: Support importing unrasterized SVG_Costumes from the “Costumes” and “Backgrounds” dialog
@@ -3348,7 +3348,7 @@ Fixes:
170201
------
-* GUI: let costume icons indicate svg costumes
+* GUI: let costume icons indicate svg costumes
170202
------
@@ -3508,7 +3508,7 @@ Fixes:
170707
------
* Objects, GUI, Store: tweak naming of instantiating to “clone”, enable inheritance by default
-* Objects, GUI: run “When I start as clone” scripts when manually cloning a sprite, only position at hand pointer if no such scripts exist
+* Objects, GUI: run “When I start as clone” scripts when manually cloning a sprite, only position at hand pointer if no such scripts exist
* Morphic, Objects: confine turtle direction readout to 0-360 degrees, thanks, Cynthia for the bug report!!
170708
@@ -3518,7 +3518,7 @@ Fixes:
170709
------
-* Objects, Threads: added experimental (only shown in dev mode) “tell ... to ..." and “ask ... for ...” primitives
+* Objects, Threads: added experimental (only shown in dev mode) “tell ... to ..." and “ask ... for ...” primitives
170711
------
@@ -3759,14 +3759,14 @@ v4.1 Features:
Fixes:
* changed keyboard shortcut indicator for “find blocks” to “^”
-* prevent Snap from “hanging” when encountering certain errors in visible stepping
+* prevent Snap from “hanging” when encountering certain errors in visible stepping
* only mark implicit parameters if no formal ones exist
* optimized thread-launch and script highlighting to a single frame instead of formerly two
* changed direction attribute of sprites to automatically confine to 0-360 degrees
* fixed rotation-bug when flipping costumes in "only turn left/right" mode"
* fixed variable renaming (“refactoring”) bugs, thanks, Bernat!
* fixed “fill” block crash when applying the same color twice
-* fixed occasional empty drop-down menu items named “close”
+* fixed occasional empty drop-down menu items named “close”
* fixed some typos
* limited sprites' direction and coordinates to finite numbers
* made block vars transient for block libraries
@@ -3925,6 +3925,10 @@ Translation Updates:
* Turkish
* Chinese
* Spanish
+
+
+=== development ===
+* New cloud API
* Russian
*** in development ***
diff --git a/widgets.js b/widgets.js
index 7d15fd1f..b15bc474 100644
--- a/widgets.js
+++ b/widgets.js
@@ -2075,6 +2075,10 @@ DialogBoxMorph.prototype.promptCredentials = function (
emlLabel = labelText('foo');
inp.add(emlLabel);
inp.add(eml);
+ inp.add(labelText('Password:'));
+ inp.add(pw1);
+ inp.add(labelText('Repeat Password:'));
+ inp.add(pw2);
}
if (purpose === 'login') {
@@ -2182,7 +2186,7 @@ DialogBoxMorph.prototype.promptCredentials = function (
if (purpose === 'login') {
checklist = [usr, pw1];
} else if (purpose === 'signup') {
- checklist = [usr, bmn, byr, eml];
+ checklist = [usr, bmn, byr, eml, pw1, pw2];
} else if (purpose === 'changePassword') {
checklist = [opw, pw1, pw2];
} else if (purpose === 'resetPassword') {
@@ -2205,12 +2209,12 @@ DialogBoxMorph.prototype.promptCredentials = function (
return false;
}
if (em.indexOf(' ') > -1 || em.indexOf('@') === -1
- || em.indexOf('.') === -1) {
+ || em.indexOf('.') === -1 || em.length < 5) {
indicate(eml, 'please provide a valid\nemail address');
return false;
}
}
- if (purpose === 'changePassword') {
+ if (purpose === 'changePassword' || purpose === 'signup') {
if (pw1.getValue().length < 6) {
indicate(pw1, 'password must be six\ncharacters or longer');
return false;
@@ -2249,6 +2253,7 @@ DialogBoxMorph.prototype.promptCredentials = function (
email: eml.getValue(),
oldpassword: opw.getValue(),
password: pw1.getValue(),
+ passwordRepeat: pw2.getValue(),
choice: agree
};
};