From 947906aab9a8bb684ae6d1ce1ddd0c3ed2505119 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 10:13:49 +0100 Subject: Backend load balancing support also: * eliminate now obsolete authentication roundtrip, * Cloud error message tweaks --- cloud.js | 1281 +++++++++++++++++++++++++++++++------------------------------- 1 file changed, 642 insertions(+), 639 deletions(-) diff --git a/cloud.js b/cloud.js index 54189d8..ed7233f 100644 --- a/cloud.js +++ b/cloud.js @@ -1,639 +1,642 @@ -/* - - cloud.js - - a backend API for SNAP! - - written by Jens Mönig - - Copyright (C) 2014 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 = '2014-May-26'; - -// Global stuff - -var Cloud; - -var SnapCloud = new Cloud( - 'https://snapcloud.miosoft.com/miocon/app/login?_app=SnapCloud' -); - -// Cloud ///////////////////////////////////////////////////////////// - -function Cloud(url) { - this.username = null; - this.password = null; // hex_sha512 hashed - this.url = url; - this.session = null; - this.api = {}; -} - -Cloud.prototype.clear = function () { - this.username = null; - this.password = null; - this.session = null; - this.api = {}; -}; - -Cloud.prototype.hasProtocol = function () { - return this.url.toLowerCase().indexOf('http') === 0; -}; - -// 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(), - responseList, - myself = this; - try { - request.open( - "GET", - (this.hasProtocol() ? '' : 'http://') - + this.url + 'Public' - + '&' - + 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 { - responseList = myself.parseResponse( - request.responseText - ); - callBack.call( - null, - responseList[0].SourceCode - ); - } - } 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.connect = function ( - 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, - true - ); - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - 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]; - if (myself.api.login) { - callBack.call(null, myself.api, 'Snap!Cloud'); - } else { - errorCall.call( - null, - 'connection failed' - ); - } - } else { - errorCall.call( - null, - myself.url, - 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 -) { - var myself = this; - this.connect( - function () { - myself.rawLogin(username, password, callBack, errorCall); - myself.disconnect(); - }, - errorCall - ); -}; - -Cloud.prototype.rawLogin = function ( - username, - password, - callBack, - errorCall -) { - // both callBack and errorCall are two-argument functions - var myself = this, - pwHash = hex_sha512("miosoft%20miocon," - + this.session.split('=')[1] + "," - + encodeURIComponent(username.toLowerCase()) + "," - + password // alreadey hex_sha512 hashed - ); - this.callService( - 'login', - function (response, url) { - if (myself.api.logout) { - myself.username = username; - myself.password = password; - callBack.call(null, response, url); - } else { - errorCall.call( - null, - 'Service catalog is not available,\nplease retry', - 'Connection Error:' - ); - } - }, - errorCall, - [username, pwHash] - ); -}; - -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; - - 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(); - - // 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(); - - 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, - [oldPW, 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(), - myself = this; - try { - request.open('GET', url, true); - request.withCredentials = true; - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - request.setRequestHeader('MioCracker', this.session); - 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, - 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 { - request.open(service.method, service.url, true); - request.withCredentials = true; - request.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - request.setRequestHeader('MioCracker', this.session); - 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); - } - 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 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-January-12'; + +// 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 = 10, + 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(), + responseList, + myself = this; + try { + request.open( + "GET", + (this.hasProtocol() ? '' : 'http://') + + this.url + 'Public' + + '?' + + 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 { + responseList = myself.parseResponse( + request.responseText + ); + callBack.call( + null, + responseList[0].SourceCode + ); + } + } 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; + + 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(); + + // 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(); + + 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); + } + 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); +}; -- cgit v1.3.1 From 50e84f8890079c45bb32e001eece4c3544fd1641 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 10:15:56 +0100 Subject: Check project for compatibility notify users of potential incompatibilities when opening projects created in other forks (e.g. BeetleBlocks) --- gui.js | 43 ++++++++++++++++++++++++++----------------- store.js | 29 +++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/gui.js b/gui.js index 8cd9403..d862463 100644 --- a/gui.js +++ b/gui.js @@ -9,7 +9,7 @@ written by Jens Mönig jens@moenig.org - Copyright (C) 2014 by Jens Mönig + Copyright (C) 2015 by Jens Mönig This file is part of Snap!. @@ -69,7 +69,7 @@ SpeechBubbleMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2014-December-04'; +modules.gui = '2015-January-12'; // Declarations @@ -300,7 +300,6 @@ IDE_Morph.prototype.openIn = function (world) { this.inform('Snap!', motd); } */ - function interpretUrlAnchors() { var dict; if (location.hash.substr(0, 6) === '#open:') { @@ -2509,7 +2508,7 @@ IDE_Morph.prototype.aboutSnap = function () { world = this.world(); aboutTxt = 'Snap! 4.0\nBuild Your Own Blocks\n\n--- beta ---\n\n' - + 'Copyright \u24B8 2014 Jens M\u00F6nig and ' + + 'Copyright \u24B8 2015 Jens M\u00F6nig and ' + 'Brian Harvey\n' + 'jens@moenig.org, bh@cs.berkeley.edu\n\n' @@ -2943,12 +2942,18 @@ IDE_Morph.prototype.rawOpenProjectString = function (str) { StageMorph.prototype.enableCodeMapping = false; if (Process.prototype.isCatchingErrors) { try { - this.serializer.openProject(this.serializer.load(str), this); + this.serializer.openProject( + this.serializer.load(str, this), + this + ); } catch (err) { this.showMessage('Load failed: ' + err); } } else { - this.serializer.openProject(this.serializer.load(str), this); + this.serializer.openProject( + this.serializer.load(str, this), + this + ); } this.stopFastTracking(); }; @@ -2980,7 +2985,10 @@ IDE_Morph.prototype.rawOpenCloudDataString = function (str) { model = this.serializer.parse(str); this.serializer.loadMediaModel(model.childNamed('media')); this.serializer.openProject( - this.serializer.loadProjectModel(model.childNamed('project')), + this.serializer.loadProjectModel( + model.childNamed('project'), + this + ), this ); } catch (err) { @@ -2990,7 +2998,10 @@ IDE_Morph.prototype.rawOpenCloudDataString = function (str) { model = this.serializer.parse(str); this.serializer.loadMediaModel(model.childNamed('media')); this.serializer.openProject( - this.serializer.loadProjectModel(model.childNamed('project')), + this.serializer.loadProjectModel( + model.childNamed('project'), + this + ), this ); } @@ -3974,6 +3985,9 @@ IDE_Morph.prototype.cloudResponse = function () { IDE_Morph.prototype.cloudError = function () { var myself = this; + // try finding an eplanation what's going on + // has some issues, commented out for now + /* function getURL(url) { try { var request = new XMLHttpRequest(); @@ -3987,13 +4001,15 @@ IDE_Morph.prototype.cloudError = function () { return null; } } + */ return function (responseText, url) { // first, try to find out an explanation for the error // and notify the user about it, // if none is found, show an error dialog box var response = responseText, - explanation = getURL('http://snap.berkeley.edu/cloudmsg.txt'); + // explanation = getURL('http://snap.berkeley.edu/cloudmsg.txt'), + explanation = null; if (myself.shield) { myself.shield.destroy(); myself.shield = null; @@ -4044,14 +4060,7 @@ IDE_Morph.prototype.setCloudURL = function () { null, { 'Snap!Cloud' : - 'https://snapcloud.miosoft.com/miocon/app/' + - 'login?_app=SnapCloud', - 'local network lab' : - '192.168.2.107:8087/miocon/app/login?_app=SnapCloud', - 'local network office' : - '192.168.186.146:8087/miocon/app/login?_app=SnapCloud', - 'localhost dev' : - 'localhost/miocon/app/login?_app=SnapCloud' + 'https://snap.apps.miosoft.com/SnapCloud' } ); }; diff --git a/store.js b/store.js index 8cbc10f..9dd851d 100644 --- a/store.js +++ b/store.js @@ -7,7 +7,7 @@ written by Jens Mönig jens@moenig.org - Copyright (C) 2014 by Jens Mönig + Copyright (C) 2015 by Jens Mönig This file is part of Snap!. @@ -61,7 +61,7 @@ SyntaxElementMorph, Variable*/ // Global stuff //////////////////////////////////////////////////////// -modules.store = '2014-December-17'; +modules.store = '2015-January-12'; // XML_Serializer /////////////////////////////////////////////////////// @@ -306,16 +306,33 @@ XML_Serializer.prototype.mediaXML = function (name) { return xml + ''; }; - // SnapSerializer loading: -SnapSerializer.prototype.load = function (xmlString) { +SnapSerializer.prototype.load = function (xmlString, ide) { // public - answer a new Project represented by the given XML String - return this.loadProjectModel(this.parse(xmlString)); + return this.loadProjectModel(this.parse(xmlString), ide); }; -SnapSerializer.prototype.loadProjectModel = function (xmlNode) { +SnapSerializer.prototype.loadProjectModel = function (xmlNode, ide) { // public - answer a new Project represented by the given XML top node + // show a warning if the origin apps differ + + var appInfo = xmlNode.attributes.app, + app = appInfo ? appInfo.split(' ')[0] : null; + + if (ide && app !== this.app.split(' ')[0]) { + ide.inform( + app + ' Project', + 'This project has been created by a different app:\n\n' + + app + + '\n\nand may be incompatible or fail to load here.' + ); + } + return this.rawLoadProjectModel(xmlNode); +}; + +SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode) { + // private var myself = this, project = {sprites: {}}, model, -- cgit v1.3.1 From 04ffda276e0294996431da8aca3584918873ae4f Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 10:17:13 +0100 Subject: Speed up messages received by clones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don’t highlight scripts running inside clones (boosts performance), Thanks, @aranlunzer, for the hint! --- threads.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/threads.js b/threads.js index 791d81c..555e531 100644 --- a/threads.js +++ b/threads.js @@ -9,7 +9,7 @@ written by Jens Mönig jens@moenig.org - Copyright (C) 2014 by Jens Mönig + Copyright (C) 2015 by Jens Mönig This file is part of Snap!. @@ -83,7 +83,7 @@ ArgLabelMorph, localize, XML_Element, hex_sha512*/ // Global stuff //////////////////////////////////////////////////////// -modules.threads = '2014-December-17'; +modules.threads = '2014-January-12'; var ThreadManager; var Process; @@ -159,9 +159,11 @@ ThreadManager.prototype.startProcess = function ( active.stop(); this.removeTerminatedProcesses(); } - top.addHighlight(); newProc = new Process(block.topBlock(), callback); newProc.exportResult = exportResult; + if (!newProc.homeContext.receiver.isClone) { + top.addHighlight(); + } this.processes.push(newProc); return newProc; }; -- cgit v1.3.1 From a0b39a4dafd41214ebf52f2db149486a2bdf27b7 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 10:17:58 +0100 Subject: Make clones non-editable Disable clones from being edited via their context menus or double-click --- objects.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/objects.js b/objects.js index b155b05..78545a5 100644 --- a/objects.js +++ b/objects.js @@ -9,7 +9,7 @@ written by Jens Mönig jens@moenig.org - Copyright (C) 2014 by Jens Mönig + Copyright (C) 2015 by Jens Mönig This file is part of Snap!. @@ -125,7 +125,7 @@ PrototypeHatBlockMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.objects = '2014-December-17'; +modules.objects = '2015-January-12'; var SpriteMorph; var StageMorph; @@ -2642,7 +2642,9 @@ SpriteMorph.prototype.userMenu = function () { menu.addItem("duplicate", 'duplicate'); menu.addItem("delete", 'remove'); menu.addItem("move", 'move'); - menu.addItem("edit", 'edit'); + if (!this.isClone) { + menu.addItem("edit", 'edit'); + } menu.addLine(); if (this.anchor) { menu.addItem( @@ -2658,6 +2660,7 @@ SpriteMorph.prototype.userMenu = function () { }; SpriteMorph.prototype.exportSprite = function () { + if (this.isCoone) {return; } var ide = this.parentThatIsA(IDE_Morph); if (ide) { ide.exportSprite(this); @@ -3530,6 +3533,7 @@ SpriteMorph.prototype.mouseClickLeft = function () { }; SpriteMorph.prototype.mouseDoubleClick = function () { + if (this.isClone) {return; } this.edit(); }; -- cgit v1.3.1 From 91838819c479ce96dc7a0c2a772912354c9b3988 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 10:18:20 +0100 Subject: Italian translation update, thanks, Alberto Firpo! --- lang-it.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- locale.js | 10 +++--- 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/lang-it.js b/lang-it.js index 44111aa..85600a0 100644 --- a/lang-it.js +++ b/lang-it.js @@ -181,11 +181,11 @@ SnapTranslator.dict.it = { 'language_name': 'Italiano', // the name as it should appear in the language menu 'language_translator': - 'Stefano Federici', // your name for the Translators tab + 'Stefano Federici, Alberto Firpo', // your name for the Translators tab 'translator_e-mail': - 's_federici@yahoo.com', // optional + 's_federici@yahoo.com, albertofirpo12@gmail.com', // optional 'last_changed': - '2012-10-16', // this, too, will appear in the Translators tab + '2015-01-12', // this, too, will appear in the Translators tab // GUI // control bar: @@ -426,6 +426,10 @@ SnapTranslator.dict.it = { 'invia a tutti %msg e attendi', 'Message name': 'Nome messaggio', + 'message': + 'messaggio', + 'any message': + 'qualunque messaggio', 'wait %n secs': 'attendi %n secondi', 'wait until %b': @@ -444,10 +448,22 @@ SnapTranslator.dict.it = { 'risultato %s', 'stop block': 'ferma il blocco', + 'all': + 'tutti', + 'this script': + 'questo script', + 'this block': + 'questo Blocco', 'stop script': 'ferma lo script', 'stop all %stop': 'ferma tutto %stop', + 'all but this script': + 'tutto tranne questo script', + 'other scripts in sprite': + 'altri script dello sprite', + 'pause all %pause': + 'pausa tutto %pause', 'run %cmdRing %inputs': 'esegui %cmdRing %inputs', 'launch %cmdRing %inputs': @@ -533,6 +549,8 @@ SnapTranslator.dict.it = { 'falso', 'join %words': 'unione di %words', + 'split %s by %delim': + 'separa %s di %delim', 'hello': 'ciao', 'world': @@ -558,6 +576,8 @@ SnapTranslator.dict.it = { 'Nuova variabile', 'Variable name': 'Nome della variabile?', + 'Script variable name': + 'Nome della variabile locale?', 'Delete a variable': 'Cancella variabile', @@ -604,6 +624,8 @@ SnapTranslator.dict.it = { // snap menu 'About...': 'Informazioni su Snap!...', + 'Reference manual': + 'Manuale', 'Snap! website': 'Sito web di Snap!', 'Download source': @@ -648,6 +670,10 @@ SnapTranslator.dict.it = { 'Importa tools', 'load the official library of\npowerful blocks': 'carica la libreria ufficiale di\nblocchi Snap', + 'Libraries...': + 'Modulo...', + 'Import library': + 'Importa modulo', // cloud menu 'Login...': @@ -660,6 +686,16 @@ SnapTranslator.dict.it = { 'Lingua...', 'Zoom blocks...': 'Zoom dei blocchi...', + 'Stage size...': + 'Dimensione pannello...', + 'Stage size': + 'Dimensione pannello', + 'Stage width': + 'Larghezza pannello', + 'Stage height': + 'Altezza pannello', + 'Default': + 'Default', 'Blurred shadows': 'Ombreggiature attenuate', 'uncheck to use solid drop\nshadows and highlights': @@ -688,6 +724,13 @@ SnapTranslator.dict.it = { 'disabilitare per permettere agli slot di espellere\ni reporter inclusi al loro interno', 'Long form input dialog': 'Usa finestra degli input estesa', + 'Plain prototype labels': + 'Etichetta prototipo base', + 'uncheck to always show (+) symbols\nin block prototype labels': + 'disabilitare per visualizzare sempre (+) \nnelle etichette dei blocchi prototipo', + 'check to hide (+) symbols\nin block prototype labels': + 'abilitare per visualizzare sempre (+) \nnelle etichette dei blocchi prototipo', + 'check to always show slot\ntypes in the input dialog': 'abilitare per mostrare sempre i tipi degli slot\nnella finestra di creazione degli input', 'uncheck to use the input\ndialog in short form': @@ -734,6 +777,13 @@ SnapTranslator.dict.it = { 'disabilitare per massima velocità\na framerate variabile', 'check for smooth, predictable\nanimations across computers': 'abilitare per avere animazioni\nfluide su tutti i computer', + 'Flat line ends': + 'fine linea piana', + 'check for flat ends of lines': + 'abilitare per fine linea netti', + 'uncheck for round ends of lines': + 'disabilitare per fine linea arrotondati', + // inputs 'with inputs': @@ -742,11 +792,20 @@ SnapTranslator.dict.it = { 'con variabili:', 'Input Names:': 'Con Variabili:', + 'input list:': + 'con liste:', + // context menus: 'help': 'aiuto', + // palette: + 'hide primitives': + 'nascondi primitive', + 'show primitives': + 'mostra primitive', + // blocks: 'help...': 'aiuto...', @@ -778,6 +837,12 @@ SnapTranslator.dict.it = { // sprites: 'edit': 'modifica', + 'move': + 'muovi', + 'detach from': + 'stacca da', + 'detach all parts': + 'stacca tutte le parti', 'export...': 'esporta...', @@ -796,6 +861,15 @@ SnapTranslator.dict.it = { 'riordina gli script\nuno sotto l\'altro', 'add comment': 'aggiungi un commento', + 'undrop': + 'annulla cancellazione', + 'undo the last\nblock drop\nin this pane': + 'annulla ultima cancellazione\ndi blocco\n in questo pannello', + 'scripts pic...': + 'immagine script...', + 'open a new window\nwith a picture of all scripts': + 'apri una nuova finestra\ncon immagine dello script', + 'make a block...': 'crea un blocco...', @@ -823,6 +897,8 @@ SnapTranslator.dict.it = { // buttons 'OK': 'OK', + 'Ok': + 'OK', 'Cancel': 'Annulla', 'Yes': @@ -1057,8 +1133,17 @@ SnapTranslator.dict.it = { 'Vuoto', // graphical effects + 'brightness': + 'Luminosita', 'ghost': 'fantasma', + 'negative': + 'negativo', + 'comic': + 'comic', + 'confetti': + 'confetti', + // keys 'space': @@ -1170,6 +1255,18 @@ SnapTranslator.dict.it = { 'e^': 'e^', + // delimiters + 'letter': + 'lettera', + 'whitespace': + 'spazio', + 'line': + 'linea', + 'tab': + 'tabulatore', + 'cr': + 'A capo', + // data types 'number': 'numero', diff --git a/locale.js b/locale.js index b3c2ce5..613ead9 100644 --- a/locale.js +++ b/locale.js @@ -6,7 +6,7 @@ written by Jens Mönig - Copyright (C) 2014 by Jens Mönig + Copyright (C) 2015 by Jens Mönig This file is part of Snap!. @@ -42,7 +42,7 @@ /*global modules, contains*/ -modules.locale = '2014-December-15'; +modules.locale = '2015-January-12'; // Global stuff @@ -157,11 +157,11 @@ SnapTranslator.dict.it = { 'language_name': 'Italiano', 'language_translator': - 'Stefano Federici', + 'Stefano Federici, Alberto Firpo', 'translator_e-mail': - 's_federici@yahoo.com', + 's_federici@yahoo.com, albertofirpo12@gmail.com', 'last_changed': - '2013-04-08' + '2015-01-12' }; SnapTranslator.dict.ja = { -- cgit v1.3.1 From 68e5feb8cef74904b0a20af6ab1455c7ce586c04 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 10:18:32 +0100 Subject: Update history --- history.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/history.txt b/history.txt index 6d6f0ff..c3c4068 100755 --- a/history.txt +++ b/history.txt @@ -2404,3 +2404,11 @@ ______ * Objects, Store: Experimental “processes” count watcher (hidden in dev mode) * Threads: Remove terminated processes from expired clones * Threads: Let “zombifying” scripts access receivers’ local vars + +150112 +------ +* Cloud, GUI: Backend load balancing support, eliminate now obsolete authentication roundtrip, Cloud error message tweaks +* Store: notify users of potential incompatibilities when opening projects created in other forks (e.g. BeetleBlocks) +* Threads: Don’t highlight scripts running inside clones (boosts performance), Thanks, @aranlunzer, for the hint! +* Objects: Disable clones from being edited via their context menus or double-click +* Italian translation update, thanks, Alberto Firpo! -- cgit v1.3.1 From ff3eed45eae2f4af3df5b636cdd59cabf9581e9b Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 10:45:21 +0100 Subject: correct threads.js version happy New Year, duh :-) --- threads.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/threads.js b/threads.js index 555e531..e07059f 100644 --- a/threads.js +++ b/threads.js @@ -83,7 +83,7 @@ ArgLabelMorph, localize, XML_Element, hex_sha512*/ // Global stuff //////////////////////////////////////////////////////// -modules.threads = '2014-January-12'; +modules.threads = '2015-January-12'; var ThreadManager; var Process; -- cgit v1.3.1 From 667193b9f02331b3372fefbb9fd4a424d5b86d7e Mon Sep 17 00:00:00 2001 From: jmoenig Date: Mon, 12 Jan 2015 13:05:14 +0100 Subject: Force Chrome to show GUI messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add additional yields to nextSteps() to work around a bug recently introduced to Chrome (other browsers don’t need this kludge). Remember to take those yields out again when and if Chrome (ever) fixes this (which, for all I know, may be never) --- gui.js | 6 ++++++ history.txt | 1 + 2 files changed, 7 insertions(+) diff --git a/gui.js b/gui.js index d862463..521f27e 100644 --- a/gui.js +++ b/gui.js @@ -352,6 +352,7 @@ IDE_Morph.prototype.openIn = function (world) { function () { msg = myself.showMessage('Opening project...'); }, + function () {nop(); }, // yield (bug in Chrome) function () { if (projectData.indexOf(' Date: Tue, 13 Jan 2015 08:36:41 +0100 Subject: fixed #702 --- byob.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/byob.js b/byob.js index b5905ce..9e7dfc8 100644 --- a/byob.js +++ b/byob.js @@ -9,7 +9,7 @@ written by Jens Mönig jens@moenig.org - Copyright (C) 2014 by Jens Mönig + Copyright (C) 2015 by Jens Mönig This file is part of Snap!. @@ -106,7 +106,7 @@ SymbolMorph, isNil*/ // Global stuff //////////////////////////////////////////////////////// -modules.byob = '2014-November-20'; +modules.byob = '2015-January-13'; // Declarations @@ -209,12 +209,14 @@ CustomBlockDefinition.prototype.copyAndBindTo = function (sprite) { c.receiver = sprite; // only for (kludgy) serialization c.declarations = copy(this.declarations); // might have to go deeper - c.body = Process.prototype.reify.call( - null, - this.body.expression, - new List(this.inputNames()) - ); - c.body.outerContext = null; + if (c.body) { + c.body = Process.prototype.reify.call( + null, + this.body.expression, + new List(this.inputNames()) + ); + c.body.outerContext = null; + } return c; }; -- cgit v1.3.1 From 4a3cf0aa3bc4778821801ec325fb2fd9dbbd038b Mon Sep 17 00:00:00 2001 From: jmoenig Date: Tue, 13 Jan 2015 08:37:33 +0100 Subject: fixed #680 --- gui.js | 11 ++++++++++- history.txt | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/gui.js b/gui.js index 521f27e..46d47e5 100644 --- a/gui.js +++ b/gui.js @@ -69,7 +69,7 @@ SpeechBubbleMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2015-January-12'; +modules.gui = '2015-January-13'; // Declarations @@ -3343,6 +3343,15 @@ IDE_Morph.prototype.toggleAppMode = function (appMode) { }).forEach(function (s) { s.adjustScrollBars(); }); + // prevent rotation and draggability controls from + // showing for the stage + if (this.currentSprite === this.stage) { + this.spriteBar.children.forEach(function (child) { + if (child instanceof PushButtonMorph) { + child.hide(); + } + }); + } } this.setExtent(this.world().extent()); // resume trackChanges }; diff --git a/history.txt b/history.txt index 3ca41f8..b621772 100755 --- a/history.txt +++ b/history.txt @@ -2413,3 +2413,8 @@ ______ * Objects: Disable clones from being edited via their context menus or double-click * Italian translation update, thanks, Alberto Firpo! * GUI: add additional yields to nextSteps() (work around a bug in Chrome) + +150113 +------ +* BYOB: fixed #702 +* GUI: fixed #680 -- cgit v1.3.1 From eb3345966e6e660b814d1e0bd3f3eb280c7c3e08 Mon Sep 17 00:00:00 2001 From: Bernat Romagosa Date: Fri, 16 Jan 2015 11:22:18 +0100 Subject: updated Catalan translation --- lang-ca.js | 84 +++++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/lang-ca.js b/lang-ca.js index 8991d45..1c9838e 100644 --- a/lang-ca.js +++ b/lang-ca.js @@ -6,7 +6,7 @@ written by Jens Mönig - Copyright (C) 2013 by Jens Mönig + Copyright (C) 2014 by Jens Mönig This file is part of Snap!. @@ -183,9 +183,9 @@ SnapTranslator.dict.ca = { 'language_translator': 'Bernat Romagosa Carrasquer', // your name for the Translators tab 'translator_e-mail': - 'tibabenfortlapalanca@gmail.com', // optional + 'bromagosa@citilab.eu', // optional 'last_changed': - '2013-11-26', // this, too, will appear in the Translators tab + '2014-01-16', // this, too, will appear in the Translators tab // GUI // control bar: @@ -447,12 +447,20 @@ SnapTranslator.dict.ca = { 'si %b llavors %c si no %c', 'report %s': 'retorna %s', - 'stop block': - 'atura el bloc', - 'stop script': - 'atura aquest programa', - 'stop all %stop': - 'atura-ho tot %stop', + 'stop %stopChoices': + 'atura %stopChoices', + 'all': + 'tot', + 'this script': + 'aquest programa', + 'this block': + 'aquest block', + 'stop %stopOthersChoices': + 'atura %stopOthersChoices', + 'all but this script': + 'tot excepte aquest programa', + 'other scripts in sprite': + 'els altres programes d\'aquest objecte', 'pause all %pause': 'pausa-ho tot %pause', 'run %cmdRing %inputs': @@ -645,7 +653,7 @@ SnapTranslator.dict.ca = { 'Import...': 'Importar...', 'file menu import hint': - 'pistes del menú d\'importació', + 'carrega una llibreria de projecte\no de blocs exportada, un vestit\no un so', 'Export project as plain text...': 'Exportar projecte en text pla...', 'Export project...': @@ -676,6 +684,16 @@ SnapTranslator.dict.ca = { 'Idioma...', 'Zoom blocks...': 'Mida dels blocs...', + 'Stage size...': + 'Mida de l\'escenari...', + 'Stage size': + 'Mida de l\'escenari', + 'Stage width': + 'Amplada de l\'escenari', + 'Stage height': + 'Alçada de l\'escenari', + 'Default': + 'Per defecte', 'Blurred shadows': 'Ombres suavitzades', 'uncheck to use solid drop\nshadows and highlights': @@ -744,9 +762,9 @@ SnapTranslator.dict.ca = { 'marca\'m per habilitar\nles animacions de la interfície', 'Thread safe scripts': 'Fil d\'execució segur', - 'uncheck to allow\nscript reentrancy': + 'uncheck to allow\nscript reentrance': 'desmarca\'m per permetre\nla re-entrada als programes', - 'check to disallow\nscript reentrancy': + 'check to disallow\nscript reentrance': 'marca\'m per no permetre\nla re-entrada als programes', 'Prefer smooth animations': 'Suavitza les animacions', @@ -754,6 +772,12 @@ SnapTranslator.dict.ca = { 'desmarca\'m per augmentar la velocitat de\nles animacions fins la màxima capacitat d\'aquesta màquina', 'check for smooth, predictable\nanimations across computers': 'marca\'m per aconseguir unes animacions\nmés suaus i a velocitat predible en màquines diferents', + 'Flat line ends': + 'Línies del llapis rectes', + 'check for flat ends of lines': + 'marca\'m per fer que els\nextrems de les línies del\nllapis siguin rectes', + 'uncheck for round ends of lines': + 'desmarca\'m per fer que\nels extrems de les línies\ndel llapis siguin arrodonits', // inputs 'with inputs': @@ -806,6 +830,8 @@ SnapTranslator.dict.ca = { // sprites: 'edit': 'editar', + 'move': + 'moure', 'detach from': 'desenganxa de', 'detach all parts': @@ -878,9 +904,9 @@ SnapTranslator.dict.ca = { // zoom blocks 'Zoom blocks': - 'Canvia la mida dels blocs', + 'Canvia la mida dels blocs', 'build': - 'fes', + 'construeix', 'your own': 'els teus propis', 'blocks': @@ -934,7 +960,7 @@ SnapTranslator.dict.ca = { // save project 'Save Project As...': - 'Anomena i desa projecte...', + 'Anomena i desa projecte...', // export blocks 'Export blocks': @@ -945,8 +971,6 @@ SnapTranslator.dict.ca = { 'aquest projecte encara no\nté cap bloc personalitzat', 'select': 'seleccionar', - 'all': - 'tots els blocs', 'none': 'cap bloc', @@ -1024,7 +1048,7 @@ SnapTranslator.dict.ca = { 'About Snap': 'Sobre Snap', 'Back...': - 'Enrera...', + 'Enrere...', 'License...': 'Llicència...', 'Modules...': @@ -1094,8 +1118,16 @@ SnapTranslator.dict.ca = { 'Buit', // graphical effects + 'brightness': + 'brillantor', 'ghost': 'fantasma', + 'negative': + 'negatiu', + 'comic': + 'còmic', + 'confetti': + 'confeti', // keys 'space': @@ -1187,29 +1219,31 @@ SnapTranslator.dict.ca = { // math functions 'abs': - 'abs', + 'valor absolut', 'floor': 'part entera', 'sqrt': 'arrel quadrada', 'sin': - 'sin', + 'sinus', 'cos': - 'cos', + 'cosinus', 'tan': - 'tan', + 'tangent', 'asin': - 'asin', + 'arcsinus', 'acos': - 'acos', + 'arccosinus', 'atan': - 'atan', + 'arctangent', 'ln': 'ln', 'e^': 'e^', // delimiters + 'letter': + 'lletra', 'whitespace': 'espai en blanc', 'line': -- cgit v1.3.1 From 3a24dacfa4063a65d92d5a9e681db1754d6930e1 Mon Sep 17 00:00:00 2001 From: Bernat Romagosa Date: Wed, 21 Jan 2015 09:42:27 +0100 Subject: updated Catalan translation --- lang-ca.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lang-ca.js b/lang-ca.js index 1c9838e..b555b15 100644 --- a/lang-ca.js +++ b/lang-ca.js @@ -1225,17 +1225,17 @@ SnapTranslator.dict.ca = { 'sqrt': 'arrel quadrada', 'sin': - 'sinus', + 'sin', 'cos': - 'cosinus', + 'cos', 'tan': - 'tangent', + 'tan', 'asin': - 'arcsinus', + 'asin', 'acos': - 'arccosinus', + 'acos', 'atan': - 'arctangent', + 'atan', 'ln': 'ln', 'e^': -- cgit v1.3.1 From f1fbb38b8715639e6025c130de70ef269cb0c0cd Mon Sep 17 00:00:00 2001 From: jmoenig Date: Wed, 21 Jan 2015 10:23:02 +0100 Subject: Keep layering of nested sprites thru drag & drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit it used to be that dragging an anchor always brought it to the front, altering the nested sprite’s internal layering order --- history.txt | 4 ++++ objects.js | 27 ++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index b621772..1880e45 100755 --- a/history.txt +++ b/history.txt @@ -2418,3 +2418,7 @@ ______ ------ * BYOB: fixed #702 * GUI: fixed #680 + +150121 +------ +* Objects: Keep layering of nested sprites thru drag & drop diff --git a/objects.js b/objects.js index 78545a5..e59726f 100644 --- a/objects.js +++ b/objects.js @@ -125,7 +125,7 @@ PrototypeHatBlockMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.objects = '2015-January-12'; +modules.objects = '2015-January-21'; var SpriteMorph; var StageMorph; @@ -1315,6 +1315,7 @@ SpriteMorph.prototype.init = function (globals) { this.anchor = null; this.nestingScale = 1; this.rotatesWithAnchor = true; + this.layers = null; // cache for dragging nested sprites, don't serialize this.blocksCache = {}; // not to be serialized (!) this.paletteCache = {}; // not to be serialized (!) @@ -3158,6 +3159,7 @@ SpriteMorph.prototype.positionTalkBubble = function () { SpriteMorph.prototype.prepareToBeGrabbed = function (hand) { var bubble = this.talkBubble(); + this.recordLayers(); if (!bubble) {return null; } this.removeShadow(); bubble.hide(); @@ -3168,6 +3170,7 @@ SpriteMorph.prototype.prepareToBeGrabbed = function (hand) { }; SpriteMorph.prototype.justDropped = function () { + this.restoreLayers(); this.positionTalkBubble(); }; @@ -4045,6 +4048,28 @@ SpriteMorph.prototype.allAnchors = function () { return result; }; +SpriteMorph.prototype.recordLayers = function () { + var stage = this.parentThatIsA(StageMorph); + if (!stage) { + this.layerCache = null; + return; + } + this.layers = this.allParts(); + this.layers.sort(function (x, y) { + return stage.children.indexOf(x) < stage.children.indexOf(y) ? + -1 : 1; + }); +}; + +SpriteMorph.prototype.restoreLayers = function () { + if (this.layers && this.layers.length > 1) { + this.layers.forEach(function (sprite) { + sprite.comeToFront(); + }); + } + this.layers = null; +}; + // SpriteMorph highlighting SpriteMorph.prototype.addHighlight = function (oldHighlight) { -- cgit v1.3.1 From fee92b65f292ee46b83cc499bf623eb269dc6565 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Wed, 21 Jan 2015 12:18:46 +0100 Subject: Generate ScriptsPaneTexture programmatically --- byob.js | 4 ++-- gui.js | 24 ++++++++++++++++++++---- history.txt | 1 + manifest.mf | 3 +-- scriptsPaneTexture.gif | Bin 155 -> 0 bytes store.js | 4 ++-- 6 files changed, 26 insertions(+), 10 deletions(-) delete mode 100755 scriptsPaneTexture.gif diff --git a/byob.js b/byob.js index 9e7dfc8..ec714f1 100644 --- a/byob.js +++ b/byob.js @@ -106,7 +106,7 @@ SymbolMorph, isNil*/ // Global stuff //////////////////////////////////////////////////////// -modules.byob = '2015-January-13'; +modules.byob = '2015-January-21'; // Declarations @@ -1651,7 +1651,7 @@ BlockEditorMorph.prototype.init = function (definition, target) { scripts = new ScriptsMorph(target); scripts.isDraggable = false; scripts.color = IDE_Morph.prototype.groupColor; - scripts.texture = IDE_Morph.prototype.scriptsPaneTexture; + scripts.cachedTexture = IDE_Morph.prototype.scriptsPaneTexture; scripts.cleanUpMargin = 10; proto = new PrototypeHatBlockMorph(this.definition); diff --git a/gui.js b/gui.js index 46d47e5..9b8b51a 100644 --- a/gui.js +++ b/gui.js @@ -69,7 +69,7 @@ SpeechBubbleMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2015-January-13'; +modules.gui = '2015-January-21'; // Declarations @@ -119,7 +119,7 @@ IDE_Morph.prototype.setDefaultDesign = function () { ]; IDE_Morph.prototype.rotationStyleColors = IDE_Morph.prototype.tabColors; IDE_Morph.prototype.appModeColor = new Color(); - IDE_Morph.prototype.scriptsPaneTexture = 'scriptsPaneTexture.gif'; + IDE_Morph.prototype.scriptsPaneTexture = this.scriptsTexture(); IDE_Morph.prototype.padding = 5; SpriteIconMorph.prototype.labelColor @@ -172,6 +172,22 @@ IDE_Morph.prototype.setFlatDesign = function () { = IDE_Morph.prototype.buttonLabelColor; }; +IDE_Morph.prototype.scriptsTexture = function () { + var pic = newCanvas(new Point(100, 100)), // bigger scales faster + ctx = pic.getContext('2d'), + i; + for (i = 0; i < 100; i += 4) { + ctx.fillStyle = this.frameColor.toString(); + ctx.fillRect(i, 0, 1, 100); + ctx.fillStyle = this.groupColor.lighter(6).toString(); + ctx.fillRect(i + 1, 0, 1, 100); + ctx.fillRect(i + 3, 0, 1, 100); + ctx.fillStyle = this.groupColor.toString(); + ctx.fillRect(i + 2, 0, 1, 100); + } + return pic; +}; + IDE_Morph.prototype.setDefaultDesign(); // IDE_Morph instance creation: @@ -1152,7 +1168,7 @@ IDE_Morph.prototype.createSpriteEditor = function () { if (this.currentTab === 'scripts') { scripts.isDraggable = false; scripts.color = this.groupColor; - scripts.texture = this.scriptsPaneTexture; + scripts.cachedTexture = this.scriptsPaneTexture; this.spriteEditor = new ScrollFrameMorph( scripts, @@ -3527,7 +3543,7 @@ IDE_Morph.prototype.userSetBlocksScale = function () { sample = new FrameMorph(); sample.acceptsDrops = false; - sample.texture = this.scriptsPaneTexture; + sample.cachedTexture = this.scriptsPaneTexture; sample.setExtent(new Point(250, 180)); scrpt.setPosition(sample.position().add(10)); sample.add(scrpt); diff --git a/history.txt b/history.txt index 1880e45..ff472c7 100755 --- a/history.txt +++ b/history.txt @@ -2422,3 +2422,4 @@ ______ 150121 ------ * Objects: Keep layering of nested sprites thru drag & drop +* GUI, Store, BYOB: Generate ScriptsPaneTexture programmatically diff --git a/manifest.mf b/manifest.mf index 353d47f..d040961 100644 --- a/manifest.mf +++ b/manifest.mf @@ -10,5 +10,4 @@ threads.js widgets.js store.js xml.js -scriptsPaneTexture.gif -snap_logo_sm.gif +snap_logo_sm.png diff --git a/scriptsPaneTexture.gif b/scriptsPaneTexture.gif deleted file mode 100755 index 846c77f..0000000 Binary files a/scriptsPaneTexture.gif and /dev/null differ diff --git a/store.js b/store.js index 9dd851d..056767f 100644 --- a/store.js +++ b/store.js @@ -61,7 +61,7 @@ SyntaxElementMorph, Variable*/ // Global stuff //////////////////////////////////////////////////////// -modules.store = '2015-January-12'; +modules.store = '2015-January-21'; // XML_Serializer /////////////////////////////////////////////////////// @@ -861,7 +861,7 @@ SnapSerializer.prototype.loadScripts = function (scripts, model) { // private var myself = this, scale = SyntaxElementMorph.prototype.scale; - scripts.texture = 'scriptsPaneTexture.gif'; + scripts.cachedTexture = IDE_Morph.prototype.scriptsPaneTexture; model.children.forEach(function (child) { var element; if (child.tag === 'script') { -- cgit v1.3.1 From 386ff338951135bed9d8b7bd33c973b46e63e89f Mon Sep 17 00:00:00 2001 From: jmoenig Date: Wed, 21 Jan 2015 12:26:54 +0100 Subject: Fix Zoom Dialog’s sample background in “flat” design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui.js | 1 + history.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/gui.js b/gui.js index 9b8b51a..73fffe9 100644 --- a/gui.js +++ b/gui.js @@ -3543,6 +3543,7 @@ IDE_Morph.prototype.userSetBlocksScale = function () { sample = new FrameMorph(); sample.acceptsDrops = false; + sample.color = IDE_Morph.prototype.groupColor; sample.cachedTexture = this.scriptsPaneTexture; sample.setExtent(new Point(250, 180)); scrpt.setPosition(sample.position().add(10)); diff --git a/history.txt b/history.txt index ff472c7..4de8ee1 100755 --- a/history.txt +++ b/history.txt @@ -2423,3 +2423,4 @@ ______ ------ * Objects: Keep layering of nested sprites thru drag & drop * GUI, Store, BYOB: Generate ScriptsPaneTexture programmatically +* GUI: Fix Zoom Dialog’s sample background in “flat” design -- cgit v1.3.1 From 669704a90adf772f534c89fb4c3732236fef2b51 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Wed, 21 Jan 2015 12:51:08 +0100 Subject: Integrated Korean and Catalan translation updates --- history.txt | 1 + lang-ca.js | 2 +- lang-ko.js | 2 +- locale.js | 10 +++++----- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/history.txt b/history.txt index 4de8ee1..ad0f53d 100755 --- a/history.txt +++ b/history.txt @@ -2424,3 +2424,4 @@ ______ * Objects: Keep layering of nested sprites thru drag & drop * GUI, Store, BYOB: Generate ScriptsPaneTexture programmatically * GUI: Fix Zoom Dialog’s sample background in “flat” design +* Updated Korean and Catalan translations, thanks, Yunjae Jang and Bernat Romagosa! diff --git a/lang-ca.js b/lang-ca.js index b555b15..c57d0d2 100644 --- a/lang-ca.js +++ b/lang-ca.js @@ -185,7 +185,7 @@ SnapTranslator.dict.ca = { 'translator_e-mail': 'bromagosa@citilab.eu', // optional 'last_changed': - '2014-01-16', // this, too, will appear in the Translators tab + '2015-01-21', // this, too, will appear in the Translators tab // GUI // control bar: diff --git a/lang-ko.js b/lang-ko.js index 3241c7f..8cedf44 100755 --- a/lang-ko.js +++ b/lang-ko.js @@ -185,7 +185,7 @@ SnapTranslator.dict.ko = { 'translator_e-mail': 'janggoons@gmail.com', // optional 'last_changed': - '2014-11-07', // this, too, will appear in the Translators tab + '2015-01-21', // this, too, will appear in the Translators tab // GUI // control bar: diff --git a/locale.js b/locale.js index 613ead9..5ec6013 100644 --- a/locale.js +++ b/locale.js @@ -42,7 +42,7 @@ /*global modules, contains*/ -modules.locale = '2015-January-12'; +modules.locale = '2015-January-21'; // Global stuff @@ -195,9 +195,9 @@ SnapTranslator.dict.ko = { 'language_translator': 'Yunjae Jang', 'translator_e-mail': - 'yunjae.jang@inc.korea.ac.kr', + 'janggoons@gmail.com', 'last_changed': - '2012-11-18' + '2015-01-21' }; SnapTranslator.dict.pt = { @@ -375,9 +375,9 @@ SnapTranslator.dict.ca = { 'language_translator': 'Bernat Romagosa Carrasquer', 'translator_e-mail': - 'tibabenfortlapalanca@gmail.com', + 'bromagosa@citilab.eu', 'last_changed': - '2013-11-26' + '2015-01-21' }; SnapTranslator.dict.fi = { -- cgit v1.3.1 From 82552d0b294092a009a40a9761eadf0b5d3e753f Mon Sep 17 00:00:00 2001 From: jmoenig Date: Wed, 21 Jan 2015 17:15:58 +0100 Subject: Fix speech bubbles of dragged nested sprites --- history.txt | 1 + objects.js | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/history.txt b/history.txt index ad0f53d..deb76a6 100755 --- a/history.txt +++ b/history.txt @@ -2425,3 +2425,4 @@ ______ * GUI, Store, BYOB: Generate ScriptsPaneTexture programmatically * GUI: Fix Zoom Dialog’s sample background in “flat” design * Updated Korean and Catalan translations, thanks, Yunjae Jang and Bernat Romagosa! +* Objects: Fix speech bubbles of dragged nested sprites diff --git a/objects.js b/objects.js index e59726f..981fb20 100644 --- a/objects.js +++ b/objects.js @@ -3155,14 +3155,11 @@ SpriteMorph.prototype.positionTalkBubble = function () { bubble.changed(); }; -// dragging and dropping adjustments b/c of talk bubbles +// dragging and dropping adjustments b/c of talk bubbles and parts SpriteMorph.prototype.prepareToBeGrabbed = function (hand) { - var bubble = this.talkBubble(); - this.recordLayers(); - if (!bubble) {return null; } this.removeShadow(); - bubble.hide(); + this.recordLayers(); if (!this.bounds.containsPoint(hand.position())) { this.setCenter(hand.position()); } @@ -4055,6 +4052,10 @@ SpriteMorph.prototype.recordLayers = function () { return; } this.layers = this.allParts(); + this.layers.forEach(function (part) { + var bubble = part.talkBubble(); + if (bubble) {bubble.hide(); } + }); this.layers.sort(function (x, y) { return stage.children.indexOf(x) < stage.children.indexOf(y) ? -1 : 1; @@ -4065,6 +4066,7 @@ SpriteMorph.prototype.restoreLayers = function () { if (this.layers && this.layers.length > 1) { this.layers.forEach(function (sprite) { sprite.comeToFront(); + sprite.positionTalkBubble(); }); } this.layers = null; -- cgit v1.3.1 From 2ca378c50bf67b6af5e16ba72af81f4f84db308c Mon Sep 17 00:00:00 2001 From: Jens Mönig Date: Wed, 21 Jan 2015 17:18:36 +0100 Subject: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf17e18..878274d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ inspired by Scratch written by Jens Mönig and Brian Harvey jens@moenig.org, bh@cs.berkeley.edu -Copyright (C) 2014 by Jens Mönig and Brian Harvey +Copyright (C) 2015 by Jens Mönig and Brian Harvey Snap! is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as -- cgit v1.3.1