From b9ec43451b9feb0c743196e2b4c3419e5ff611f8 Mon Sep 17 00:00:00 2001 From: Gubolin Date: Wed, 30 Jul 2014 18:23:33 +0200 Subject: replace the Snap!Cloud with GitHub --- gui.js | 409 +++-------------------------------------------------------------- 1 file changed, 15 insertions(+), 394 deletions(-) (limited to 'gui.js') diff --git a/gui.js b/gui.js index e4c75dd..9850184 100644 --- a/gui.js +++ b/gui.js @@ -69,7 +69,7 @@ SpeechBubbleMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2014-July-29'; +modules.gui = '2014-July-30'; // Declarations @@ -243,8 +243,8 @@ IDE_Morph.prototype.openIn = function (world) { if (usr) { usr = SnapCloud.parseResponse(usr)[0]; if (usr) { - SnapCloud.username = usr.username || null; - SnapCloud.password = usr.password || null; + SnapCloud.login(usr.username, usr.password, + function() {}, myself.cloudError()); } } } @@ -338,12 +338,11 @@ IDE_Morph.prototype.openIn = function (world) { this.parent.add(this.shield); myself.showMessage('Fetching project\nfrom the cloud...'); - // make sure to lowercase the username dict = SnapCloud.parseDict(location.hash.substr(9)); - dict.Username = dict.Username.toLowerCase(); + dict.Username = dict.Username; SnapCloud.getPublicProject( - SnapCloud.encodeDict(dict), + dict, function (projectData) { var msg; myself.nextSteps([ @@ -375,8 +374,6 @@ IDE_Morph.prototype.openIn = function (world) { urlLanguage = location.hash.substr(6); this.setLanguage(urlLanguage); this.loadNewProject = true; - } else if (location.hash.substr(0, 7) === '#signup') { - this.createCloudAccount(); } } @@ -1935,37 +1932,16 @@ IDE_Morph.prototype.cloudMenu = function () { shiftClicked = (world.currentKey === 16); menu = new MenuMorph(this); - if (shiftClicked) { - menu.addItem( - 'url...', - 'setCloudURL', - null, - new Color(100, 0, 0) - ); - menu.addLine(); - } if (!SnapCloud.username) { menu.addItem( 'Login...', 'initializeCloud' ); - menu.addItem( - 'Signup...', - 'createCloudAccount' - ); - menu.addItem( - 'Reset Password...', - 'resetCloudPassword' - ); } else { menu.addItem( localize('Logout') + ' ' + SnapCloud.username, 'logout' ); - menu.addItem( - 'Change Password...', - 'changeCloudPassword' - ); } if (shiftClicked) { menu.addLine(); @@ -2011,53 +1987,6 @@ IDE_Morph.prototype.cloudMenu = function () { null, new Color(100, 0, 0) ); - menu.addLine(); - menu.addItem( - 'open shared project from cloud...', - 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, - function (projectData) { - var msg; - if (!Process.prototype.isCatchingErrors) { - window.open( - 'data:text/xml,' + projectData - ); - } - myself.nextSteps([ - function () { - msg = myself.showMessage( - 'Opening project...' - ); - }, - function () { - myself.rawOpenCloudDataString( - projectData - ); - }, - function () { - msg.destroy(); - } - ]); - }, - myself.cloudError() - ); - - }, null, 'project'); - }, null, 'project'); - }, - null, - new Color(100, 0, 0) - ); } menu.popup(world, pos); }; @@ -3623,17 +3552,17 @@ IDE_Morph.prototype.initializeCloud = function () { new DialogBoxMorph( null, function (user) { - var pwh = hex_sha512(user.password), + var pw = user.password, str; SnapCloud.login( user.username, - pwh, + pw, function () { if (user.choice) { str = SnapCloud.encodeDict( { username: user.username, - password: pwh + password: pw } ); localStorage['-snap-user'] = str; @@ -3658,128 +3587,14 @@ 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.email, - function (txt, title) { - new DialogBoxMorph().inform( - title, - txt + - '.\n\nAn e-mail with your password\n' + - 'has been sent to the address provided', - world, - myself.cloudIcon(null, new Color(0, 180, 0)) - ); - }, - myself.cloudError() - ); - } - ).withKey('cloudsignup').promptCredentials( - 'Sign up', - 'signup', - 'http://snap.berkeley.edu/tos.html', - 'Terms of Service...', - 'http://snap.berkeley.edu/privacy.html', - 'Privacy...', - 'I have read and agree\nto the Terms of Service', - world, - myself.cloudIcon(), - myself.cloudMsg - ); -}; - -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) { - SnapCloud.resetPassword( - user.username, - function (txt, title) { - new DialogBoxMorph().inform( - title, - txt + - '.\n\nAn e-mail with a link to\n' + - 'reset your password\n' + - 'has been sent to the address provided', - world, - myself.cloudIcon(null, new Color(0, 180, 0)) - ); - }, - myself.cloudError() - ); - } - ).withKey('cloudresetpassword').promptCredentials( - 'Reset password', - 'resetPassword', - null, - null, - null, - null, - null, - world, - myself.cloudIcon(), - myself.cloudMsg - ); -}; - -IDE_Morph.prototype.changeCloudPassword = function () { - var myself = this, - world = this.world(); - new DialogBoxMorph( - null, - function (user) { - SnapCloud.changePassword( - user.oldpassword, - user.password, - function () { - myself.logout(); - myself.showMessage('password has been changed.', 2); - }, - myself.cloudError() - ); - } - ).withKey('cloudpassword').promptCredentials( - 'Change Password', - 'changePassword', - null, - null, - null, - null, - null, - world, - myself.cloudIcon(), - myself.cloudMsg - ); -}; - 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); } ); @@ -4010,31 +3825,6 @@ IDE_Morph.prototype.cloudIcon = function (height, color) { return icon; }; -IDE_Morph.prototype.setCloudURL = function () { - new DialogBoxMorph( - null, - function (url) { - SnapCloud.url = url; - } - ).withKey('cloudURL').prompt( - 'Cloud URL', - SnapCloud.url, - this.world(), - 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' - } - ); -}; - // IDE_Morph synchronous Http data fetching IDE_Morph.prototype.getURL = function (url) { @@ -4142,8 +3932,6 @@ ProjectDialogMorph.prototype.init = function (ide, task) { this.notesText = null; this.notesField = null; this.deleteButton = null; - this.shareButton = null; - this.unshareButton = null; // initialize inherited properties: ProjectDialogMorph.uber.init.call( @@ -4283,10 +4071,6 @@ ProjectDialogMorph.prototype.buildContents = function () { this.addButton('saveProject', 'Save'); this.action = 'saveProject'; } - this.shareButton = this.addButton('shareProject', 'Share'); - this.unshareButton = this.addButton('unshareProject', 'Unshare'); - this.shareButton.hide(); - this.unshareButton.hide(); this.deleteButton = this.addButton('deleteProject', 'Delete'); this.addButton('cancel', 'Cancel'); @@ -4516,8 +4300,6 @@ ProjectDialogMorph.prototype.setSource = function (source) { }; } this.body.add(this.listField); - this.shareButton.hide(); - this.unshareButton.hide(); if (this.source === 'local') { this.deleteButton.show(); } else { // examples @@ -4594,12 +4376,7 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { function (element) { return element.ProjectName; } : null, - [ // format: display shared project names bold - [ - 'bold', - function (proj) {return proj.Public === 'true'; } - ] - ], + [], function () {myself.ok(); } ); this.fixListFieldItemColors(); @@ -4635,20 +4412,11 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { myself.preview.rightCenter().add(new Point(2, 0)) ); } - if (item.Public === 'true') { - myself.shareButton.hide(); - myself.unshareButton.show(); - } else { - myself.unshareButton.hide(); - myself.shareButton.show(); - } myself.buttons.fixLayout(); myself.fixLayout(); myself.edit(); }; this.body.add(this.listField); - this.shareButton.show(); - this.unshareButton.hide(); this.deleteButton.show(); this.buttons.fixLayout(); this.fixLayout(); @@ -4700,24 +4468,12 @@ ProjectDialogMorph.prototype.openCloudProject = function (project) { ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) { var myself = this; - SnapCloud.reconnect( - function () { - SnapCloud.callService( - 'getProject', - function (response) { - SnapCloud.disconnect(); - myself.ide.source = 'cloud'; - myself.ide.droppedText(response[0].SourceCode); - if (proj.Public === 'true') { - location.hash = '#present:Username=' + - encodeURIComponent(SnapCloud.username) + - '&ProjectName=' + - encodeURIComponent(proj.ProjectName); - } - }, - myself.ide.cloudError(), - [proj.ProjectName] - ); + SnapCloud.getProject( + {'projectname': proj.ProjectName, 'username': SnapCloud.username}, + function (code, media) { + myself.ide.source = 'cloud'; + myself.ide.droppedText(media); + myself.ide.droppedText(code); }, myself.ide.cloudError() ); @@ -4791,141 +4547,6 @@ ProjectDialogMorph.prototype.saveCloudProject = function () { this.destroy(); }; -ProjectDialogMorph.prototype.deleteProject = function () { - var myself = this, - proj, - idx, - name; - - if (this.source === 'cloud') { - proj = this.listField.selected; - if (proj) { - this.ide.confirm( - localize( - 'Are you sure you want to delete' - ) + '\n"' + proj.ProjectName + '"?', - 'Delete Project', - function () { - SnapCloud.reconnect( - 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.cloudError() - ); - } - ); - } - } else { // 'local, examples' - if (this.listField.selected) { - name = this.listField.selected.name; - this.ide.confirm( - localize( - 'Are you sure you want to delete' - ) + '\n"' + name + '"?', - 'Delete Project', - function () { - delete localStorage['-snap-project-' + name]; - myself.setSource(myself.source); // refresh list - } - ); - } - } -}; - -ProjectDialogMorph.prototype.shareProject = function () { - var myself = this, - proj = this.listField.selected, - entry = this.listField.active; - - if (proj) { - this.ide.confirm( - localize( - 'Are you sure you want to publish' - ) + '\n"' + proj.ProjectName + '"?', - 'Share Project', - function () { - myself.ide.showMessage('sharing\nproject...'); - SnapCloud.reconnect( - 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] - ); - }, - myself.ide.cloudError() - ); - } - ); - } -}; - -ProjectDialogMorph.prototype.unshareProject = function () { - var myself = this, - 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', - function () { - myself.ide.showMessage('unsharing\nproject...'); - SnapCloud.reconnect( - 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] - ); - }, - myself.ide.cloudError() - ); - } - ); - } -}; - ProjectDialogMorph.prototype.edit = function () { if (this.nameField) { this.nameField.edit(); -- cgit v1.3.1 From 8362eac883a067e04633190ff98d060d7b627ca2 Mon Sep 17 00:00:00 2001 From: Gubolin Date: Thu, 31 Jul 2014 11:34:37 +0200 Subject: merge Snap!Cloud and GitHub; fix some bugs --- cloud.js | 620 +++++++++++++++++++++++++++++++++++++++++------------ github.js | 273 ++++++++++++++++++++++++ gui.js | 721 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- snap.html | 3 +- 4 files changed, 1464 insertions(+), 153 deletions(-) create mode 100644 github.js (limited to 'gui.js') diff --git a/cloud.js b/cloud.js index 02cc14f..54189d8 100644 --- a/cloud.js +++ b/cloud.js @@ -1,11 +1,12 @@ /* -cloud.js - a GitHub backend API for SNAP! + cloud.js - written by Gubolin, based on cloud.js by Jens Mönig + a backend API for SNAP! - Copyright (C) 2014 by Jens Mönig, Gubolin + written by Jens Mönig + + Copyright (C) 2014 by Jens Mönig This file is part of Snap!. @@ -29,73 +30,253 @@ cloud.js /*global modules, IDE_Morph, SnapSerializer, hex_sha512, alert, nop, localize*/ -modules.cloud = '2014-July-29'; +modules.cloud = '2014-May-26'; // Global stuff var Cloud; -var SnapCloud = new Cloud(); +var SnapCloud = new Cloud( + 'https://snapcloud.miosoft.com/miocon/app/login?_app=SnapCloud' +); // Cloud ///////////////////////////////////////////////////////////// function Cloud(url) { - this.gh = null; this.username = null; - this.password = null; // TODO saved as plain text + this.password = null; // hex_sha512 hashed + this.url = url; this.session = null; this.api = {}; } Cloud.prototype.clear = function () { - this.gh = null; 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.getProject = function ( - dict, +Cloud.prototype.signup = function ( + username, + email, callBack, errorCall ) { - var myself = this; - var projectName = dict.projectname; - var userName = dict.username; - - if (myself.gh !== null) { - var repo = myself.gh.getRepo(userName, projectName); - var branch = repo.getBranch(); // master (default) - var media, pdata; - var br; - - branch.read('snap.xml', false).then( - function (sourceContent) { - branch.read('media.xml', false).then( // true for binary - function (mediaContent) { + // 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, - sourceContent.content, - mediaContent.content + request.responseText, + 'Signup' ); - }, - function (error) { - errorCall.call(this, error, 'Github'); } - ); - }, - function (error) { - errorCall.call(this, error, 'Github'); + } 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" ); - } else { - errorCall.call(null, 'Please login', 'Github'); + 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, @@ -103,22 +284,61 @@ Cloud.prototype.login = function ( errorCall ) { var myself = this; + this.connect( + function () { + myself.rawLogin(username, password, callBack, errorCall); + myself.disconnect(); + }, + errorCall + ); +}; - myself.gh = new Octokit({ - username: username, - password: password - }); - - myself.gh.getUser().getInfo().then( - function(info) { - myself.username = username; - myself.password = password; - - callBack.call(myself); +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:' + ); + } }, - function (error) { - errorCall.call(this, error, 'Github'); - } + 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 ); }; @@ -126,7 +346,6 @@ Cloud.prototype.saveProject = function (ide, callBack, errorCall) { var myself = this, pdata, media; - var repoName = ide.projectName.replace(/[^\w-]/g, ''); // FIXME validation of project name ide.serializer.isCollectingMedia = true; pdata = ide.serializer.serialize(ide.stage); @@ -153,94 +372,215 @@ Cloud.prototype.saveProject = function (ide, callBack, errorCall) { ide.serializer.isCollectingMedia = false; ide.serializer.flushMedia(); - myself.getProjectList( - function (projects) { - var exists = false; - - projects.forEach(function (project) { - if (project.ProjectName.indexOf(repoName) > -1) { - exists = true; - return; - } - }); - - if (exists === false){ - myself.gh.getUser().createRepo(repoName, { // these should be discussed - 'description': 'Snap! Project', - 'has_wiki': 'false', - 'has_downloads': 'false', - 'auto_init': true, - 'license_template': 'mit' // discuss - }).then( - function () {}, - function (error) { - errorCall.call(this, error, 'Github'); - } - ); - } - - if (myself.gh !== null) { - var repo = myself.gh.getRepo(myself.username, ide.projectName); - var branch = repo.getBranch(); // master (default) - var message = ''; // FIXME optional: specify message - - var contents = { - 'snap.xml': pdata, - 'media.xml': media // may be binary - }; - - branch.writeMany(contents, message).then( - function () { - callBack.call() - }, - function (error) { - errorCall.call(this, error, 'Github'); - } - ); - } - }, - function (error) { - errorCall.call(null, error, 'Github'); - } + 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 + ); +}; - if (myself.gh !== null) { - myself.gh.getUser().getRepos().then( - function (repos) { - var snapProjects = []; +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 + ); +}; - repos.forEach(function (repo) { - if (repo.description === 'Snap! Project') { // FIXME nicer detection - var project; +Cloud.prototype.logout = function (callBack, errorCall) { + this.clear(); + this.callService( + 'logout', + callBack, + errorCall + ); +}; - project = { - 'ProjectName': repo.name - }; +Cloud.prototype.disconnect = function () { + this.callService( + 'logout', + nop, + nop + ); +}; - snapProjects.push(project); - } - }); +// Cloud: backend communication - callBack.call(myself, snapProjects); - }, - function (error) { - errorCall.call(this, error, 'Github'); - } +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" ); - } else { - errorCall.call(myself, 'Please login', 'Github'); + 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.logout = function (callBack) { - this.clear(); +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: backend communication +// 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 = [], @@ -248,16 +588,16 @@ Cloud.prototype.parseResponse = function (src) { 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); + 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; }; @@ -266,8 +606,8 @@ Cloud.prototype.parseDict = function (src) { if (!src) {return dict; } src.split("&").forEach(function (entry) { var pair = entry.split("="), - key = decodeURIComponent(pair[0]), - val = decodeURIComponent(pair[1]); + key = decodeURIComponent(pair[0]), + val = decodeURIComponent(pair[1]); dict[key] = val; }); return dict; @@ -279,8 +619,10 @@ Cloud.prototype.encodeDict = function (dict) { key; if (!dict) {return null; } for (key in dict) { - if (dict.hasOwnProperty(key)) { - pair = encodeURIComponent(key) + '=' + encodeURIComponent(dict[key]); + if (dict.hasOwnProperty(key)) { + pair = encodeURIComponent(key) + + '=' + + encodeURIComponent(dict[key]); if (str.length > 0) { str += '&'; } @@ -289,3 +631,9 @@ Cloud.prototype.encodeDict = function (dict) { } return str; }; + +// Cloud: user messages (to be overridden) + +Cloud.prototype.message = function (string) { + alert(string); +}; diff --git a/github.js b/github.js new file mode 100644 index 0000000..a3b3223 --- /dev/null +++ b/github.js @@ -0,0 +1,273 @@ +/* + + github.js + + a GitHubBackend backend API for SNAP! + + written by Gubolin, based on cloud.js by Jens Mönig + + Copyright (C) 2014 by Jens Mönig, Gubolin + + 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.github = '2014-July-31'; + +// Global stuff + +var GitHubBackend; + +var GitHub = new GitHubBackend(); + +// GitHubBackend ///////////////////////////////////////////////////////////// + +function GitHubBackend(url) { + this.gh = null; + this.username = null; + this.password = null; // TODO saved as plain text +} + +GitHubBackend.prototype.clear = function () { + this.gh = null; + this.username = null; + this.password = null; +}; + +// GitHubBackend: Snap! API + +GitHubBackend.prototype.getProject = function ( + userName, + projectName, + callBack, + errorCall +) { + var myself = this; + + if (myself.gh === null) { + myself.gh = new Octokit(); + } + + var repo = myself.gh.getRepo(userName, projectName); + var branch = repo.getBranch(); // master (default) + var media, pdata; + + branch.read('snap.xml', false).then( + function (sourceContent) { + branch.read('media.xml', false).then( // true for binary + function (mediaContent) { + callBack.call( + null, + sourceContent.content, + mediaContent.content + ); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); +}; + +GitHubBackend.prototype.login = function ( + username, + password, + validateData, + callBack, + errorCall +) { + var myself = this; + var me; + + myself.gh = new Octokit({ + username: username, + password: password + }); + + if (validateData === true) { + me = myself.gh.getUser(); + if (me !== null) { + me.getInfo().then( + function(info) { + myself.username = username; + myself.password = password; + + callBack.call(myself); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + errorCall.call(myself, localize('Something went wrong :('), 'GitHub'); + } + } else { + myself.username = username; + myself.password = password; + + callBack.call(myself); + } +}; + +GitHubBackend.prototype.saveProject = function (ide, callBack, errorCall) { + var myself = this, + pdata, + media; + var repoName = ide.projectName.replace(/[^\w-]/g, ''); // TODO validation of project name + + 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.getProjectList( + function (projects) { + var exists = false; + + projects.forEach(function (project) { + if (project.ProjectName.indexOf(repoName) > -1) { + exists = true; + return; + } + }); + + if (exists === false){ + myself.gh.getUser().createRepo(repoName, { // these should be discussed + 'description': 'Snap! Project - http://snap.berkeley.edu/snapsource/snap.html#github:Username=' + myself.username + '&projectName=' + repoName, + 'has_wiki': 'false', + 'has_downloads': 'false', + 'auto_init': true, + 'license_template': 'mit' // discuss + }).then( + function () {}, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } + + if (myself.gh !== null) { + var repo = myself.gh.getRepo(myself.username, ide.projectName); + var branch = repo.getBranch(); // master (default) + var message = ''; // TODO optional: specify message + + var contents = { + 'snap.xml': pdata, + 'media.xml': media, // may be binary + 'README.md': ide.projectNotes + }; + + branch.writeMany(contents, message).then( + function () { + callBack.call(); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } + }, + function (error) { + errorCall.call(null, error, 'GitHub'); + } + ); +}; + +GitHubBackend.prototype.getProjectList = function (callBack, errorCall) { + var myself = this; + + if (myself.gh !== null) { + myself.gh.getUser().getRepos().then( + function (repos) { + var snapProjects = []; + + var modCallBack = (function () { + var called = 0; + return function () { + if (++called == repos.length) { + callBack.call(myself, snapProjects); + } + }; + })(); + + repos.forEach(function (repo) { + if (repo.description.indexOf('Snap! Project') > -1) { // TODO nicer detection + var project, ghrepo, branch; + + ghrepo = myself.gh.getRepo(repo.owner.login, repo.name); + branch = ghrepo.getBranch(); // master (default) + + branch.read('README.md', false).then( + function (notesContent) { + project = { + 'ProjectName': repo.name, + 'Notes': notesContent.content, + 'Updated': repo.updated_at.replace(/T/, ' ').replace(/Z/, '') // TODO this could be better + }; + + snapProjects.push(project); + modCallBack(); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + modCallBack(); + } + }); + }, + function (error) { + errorCall.call(this, error, 'GitHub'); + } + ); + } else { + errorCall.call(myself, localize('Please login'), 'GitHub'); + } +}; + +GitHubBackend.prototype.logout = function (callBack) { + this.clear(); +}; diff --git a/gui.js b/gui.js index a4e1dd5..cbc3353 100644 --- a/gui.js +++ b/gui.js @@ -63,13 +63,13 @@ Costume, CostumeEditorMorph, MorphicPreferences, touchScreenSettings, standardSettings, Sound, BlockMorph, ToggleMorph, InputSlotDialogMorph, ScriptsMorph, isNil, SymbolMorph, BlockExportDialogMorph, BlockImportDialogMorph, SnapTranslator, localize, List, InputSlotMorph, -SnapCloud, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512, +SnapCloud, GitHub, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512, sb, CommentMorph, CommandBlockMorph, BlockLabelPlaceHolderMorph, Audio, SpeechBubbleMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2014-July-30'; +modules.gui = '2014-July-31'; // Declarations @@ -240,6 +240,7 @@ IDE_Morph.prototype.openIn = function (world) { // get persistent user data, if any if (localStorage) { usr = localStorage['-snap-user']; + ghusr = localStorage['-snap-ghuser']; if (usr) { usr = SnapCloud.parseResponse(usr)[0]; if (usr) { @@ -247,6 +248,13 @@ IDE_Morph.prototype.openIn = function (world) { function() {}, myself.cloudError()); } } + if (ghusr) { + ghusr = SnapCloud.parseResponse(ghusr)[0]; + if (ghusr) { + GitHub.login(ghusr.username, ghusr.password, false, + function() {}, myself.githubError()); + } + } } // override SnapCloud's user message with Morphic @@ -338,11 +346,12 @@ IDE_Morph.prototype.openIn = function (world) { this.parent.add(this.shield); myself.showMessage('Fetching project\nfrom the cloud...'); + // make sure to lowercase the username dict = SnapCloud.parseDict(location.hash.substr(9)); - dict.Username = dict.Username; + dict.Username = dict.Username.toLowerCase(); SnapCloud.getPublicProject( - dict, + SnapCloud.encodeDict(dict), function (projectData) { var msg; myself.nextSteps([ @@ -370,10 +379,51 @@ IDE_Morph.prototype.openIn = function (world) { }, this.cloudError() ); + } else if (location.hash.substr(0, 8) === '#github:') { + this.shield = new Morph(); + this.shield.color = this.color; + this.shield.setExtent(this.parent.extent()); + this.parent.add(this.shield); + myself.showMessage('Fetching project\nfrom GitHub...'); + + dict = SnapCloud.parseDict(location.hash.substr(8)); + + GitHub.getProject( + dict.Username, + dict.projectName, + function (projectData) { + var msg; + myself.nextSteps([ + function () { + msg = myself.showMessage('Opening GitHub project...'); + }, + function () { + if (projectData.indexOf(' 50) { + response = response.substring(0, 50) + '...'; + } + new DialogBoxMorph().inform( + 'GitHub', + (url ? url + '\n' : '') + + response, + myself.world(), + myself.cloudIcon(null, new Color(180, 0, 0)) + ); + }; +}; + IDE_Morph.prototype.cloudIcon = function (height, color) { var clr = color || DialogBoxMorph.prototype.titleBarColor, isFlat = MorphicPreferences.isFlat, @@ -3827,6 +4182,31 @@ IDE_Morph.prototype.cloudIcon = function (height, color) { return icon; }; +IDE_Morph.prototype.setCloudURL = function () { + new DialogBoxMorph( + null, + function (url) { + SnapCloud.url = url; + } + ).withKey('cloudURL').prompt( + 'Cloud URL', + SnapCloud.url, + this.world(), + 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' + } + ); +}; + // IDE_Morph synchronous Http data fetching IDE_Morph.prototype.getURL = function (url) { @@ -3923,7 +4303,7 @@ ProjectDialogMorph.prototype.init = function (ide, task) { // additional properties: this.ide = ide; this.task = task || 'open'; // String describing what do do (open, save) - this.source = ide.source || 'local'; // or 'cloud' or 'examples' + this.source = ide.source || 'local'; // 'or 'cloud' or 'github' or 'examples' this.projectList = []; // [{name: , thumb: , notes:}] this.handle = null; @@ -3934,6 +4314,8 @@ ProjectDialogMorph.prototype.init = function (ide, task) { this.notesText = null; this.notesField = null; this.deleteButton = null; + this.shareButton = null; + this.unshareButton = null; // initialize inherited properties: ProjectDialogMorph.uber.init.call( @@ -3981,6 +4363,7 @@ ProjectDialogMorph.prototype.buildContents = function () { } this.addSourceButton('cloud', localize('Cloud'), 'cloud'); + this.addSourceButton('github', localize('GitHub'), 'github'); this.addSourceButton('local', localize('Browser'), 'storage'); if (this.task === 'open') { this.addSourceButton('examples', localize('Examples'), 'poster'); @@ -4073,6 +4456,10 @@ ProjectDialogMorph.prototype.buildContents = function () { this.addButton('saveProject', 'Save'); this.action = 'saveProject'; } + this.shareButton = this.addButton('shareProject', 'Share'); + this.unshareButton = this.addButton('unshareProject', 'Unshare'); + this.shareButton.hide(); + this.unshareButton.hide(); this.deleteButton = this.addButton('deleteProject', 'Delete'); this.addButton('cancel', 'Cancel'); @@ -4225,6 +4612,20 @@ ProjectDialogMorph.prototype.setSource = function (source) { } ); return; + case 'github': + msg = myself.ide.showMessage('Updating\nproject list...'); + this.projectList = []; + GitHub.getProjectList( + function (projectList) { + myself.installGitHubProjectList(projectList); + msg.destroy(); + }, + function (err, lbl) { + msg.destroy(); + myself.ide.githubError().call(null, err, lbl); + } + ); + return; case 'examples': this.projectList = this.getExamplesProjectList(); break; @@ -4277,7 +4678,7 @@ ProjectDialogMorph.prototype.setSource = function (source) { } myself.edit(); }; - } else { // 'examples', 'cloud' is initialized elsewhere + } else { // 'examples', 'cloud' and 'github' is initialized elsewhere this.listField.action = function (item) { var src, xml; if (item === undefined) {return; } @@ -4302,6 +4703,8 @@ ProjectDialogMorph.prototype.setSource = function (source) { }; } this.body.add(this.listField); + this.shareButton.hide(); + this.unshareButton.hide(); if (this.source === 'local') { this.deleteButton.show(); } else { // examples @@ -4371,6 +4774,83 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { return x.ProjectName < y.ProjectName ? -1 : 1; }); + this.listField.destroy(); + this.listField = new ListMorph( + this.projectList, + this.projectList.length > 0 ? + function (element) { + return element.ProjectName; + } : null, + [ // format: display shared project names bold + [ + 'bold', + function (proj) {return proj.Public === 'true'; } + ] + ], + function () {myself.ok(); } + ); + this.fixListFieldItemColors(); + this.listField.fixLayout = nop; + this.listField.edge = InputFieldMorph.prototype.edge; + this.listField.fontSize = InputFieldMorph.prototype.fontSize; + this.listField.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.listField.contrast = InputFieldMorph.prototype.contrast; + this.listField.drawNew = InputFieldMorph.prototype.drawNew; + this.listField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + this.listField.action = function (item) { + if (item === undefined) {return; } + if (myself.nameField) { + myself.nameField.setContents(item.ProjectName || ''); + } + if (myself.task === 'open') { + myself.notesText.text = item.Notes || ''; + myself.notesText.drawNew(); + myself.notesField.contents.adjustBounds(); + myself.preview.texture = item.Thumbnail || null; + myself.preview.cachedTexture = null; + myself.preview.drawNew(); + (new SpeechBubbleMorph(new TextMorph( + localize('last changed') + '\n' + item.Updated, + null, + null, + null, + null, + 'center' + ))).popUp( + myself.world(), + myself.preview.rightCenter().add(new Point(2, 0)) + ); + } + if (item.Public === 'true') { + myself.shareButton.hide(); + myself.unshareButton.show(); + } else { + myself.unshareButton.hide(); + myself.shareButton.show(); + } + myself.buttons.fixLayout(); + myself.fixLayout(); + myself.edit(); + }; + this.body.add(this.listField); + this.shareButton.show(); + this.unshareButton.hide(); + this.deleteButton.show(); + this.buttons.fixLayout(); + this.fixLayout(); + if (this.task === 'open') { + this.clearDetails(); + } +}; + +ProjectDialogMorph.prototype.installGitHubProjectList = function (pl) { + var myself = this; + this.projectList = pl || []; + this.projectList.sort(function (x, y) { + return x.ProjectName < y.ProjectName ? -1 : 1; + }); + this.listField.destroy(); this.listField = new ListMorph( this.projectList, @@ -4443,6 +4923,8 @@ ProjectDialogMorph.prototype.openProject = function () { this.ide.source = this.source; if (this.source === 'cloud') { this.openCloudProject(proj); + } else if (this.source === 'github') { + this.openGitHubProject(proj); } else if (this.source === 'examples') { src = this.ide.getURL( 'http://snap.berkeley.edu/snapsource/Examples/' + @@ -4468,20 +4950,58 @@ ProjectDialogMorph.prototype.openCloudProject = function (project) { ]); }; +ProjectDialogMorph.prototype.openGitHubProject = function (project) { + var myself = this; + myself.ide.nextSteps([ + function () { + myself.ide.showMessage('Fetching project\nfrom GitHub...'); + }, + function () { + myself.rawOpenGitHubProject(project); + } + ]); +}; + ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) { var myself = this; - SnapCloud.getProject( - {'projectname': proj.ProjectName, 'username': SnapCloud.username}, + SnapCloud.reconnect( + function () { + SnapCloud.callService( + 'getProject', + function (response) { + SnapCloud.disconnect(); + myself.ide.source = 'cloud'; + myself.ide.droppedText(response[0].SourceCode); + if (proj.Public === 'true') { + location.hash = '#present:Username=' + + encodeURIComponent(SnapCloud.username) + + '&ProjectName=' + + encodeURIComponent(proj.ProjectName); + } + }, + myself.ide.cloudError(), + [proj.ProjectName] + ); + }, + myself.ide.cloudError() + ); + this.destroy(); +}; + +ProjectDialogMorph.prototype.rawOpenGitHubProject = function (proj) { + var myself = this; + GitHub.getProject( + GitHub.username, + proj.ProjectName, function (code, media) { - myself.ide.source = 'cloud'; + myself.ide.source = 'github'; myself.ide.droppedText(media); myself.ide.droppedText(code); }, - myself.ide.cloudError() + myself.ide.githubError() ); this.destroy(); }; - ProjectDialogMorph.prototype.saveProject = function () { var name = this.nameField.contents().text.text, notes = this.notesText.text, @@ -4508,6 +5028,25 @@ ProjectDialogMorph.prototype.saveProject = function () { this.ide.setProjectName(name); myself.saveCloudProject(); } + } else if (this.source === 'github') { + if (detect( + this.projectList, + function (item) {return item.ProjectName === name; } + )) { + this.ide.confirm( + localize( + 'Are you sure you want to replace' + ) + '\n"' + name + '"?', + 'Replace Project', + function () { + myself.ide.setProjectName(name); + myself.saveGitHubProject(); + } + ); + } else { + this.ide.setProjectName(name); + myself.saveGitHubProject(); + } } else { // 'local' if (detect( this.projectList, @@ -4549,6 +5088,156 @@ ProjectDialogMorph.prototype.saveCloudProject = function () { this.destroy(); }; +ProjectDialogMorph.prototype.saveGitHubProject = function () { + var myself = this; + this.ide.showMessage('Committing project\nto GitHub...'); + GitHub.saveProject( + this.ide, + function () { + myself.ide.source = 'github'; + myself.ide.showMessage('saved.', 2); + }, + this.ide.githubError() + ); + this.destroy(); +}; + +ProjectDialogMorph.prototype.deleteProject = function () { + var myself = this, + proj, + idx, + name; + + if (this.source === 'github') { + // TODO: not implemented + } else if (this.source === 'cloud') { + proj = this.listField.selected; + if (proj) { + this.ide.confirm( + localize( 'Are you sure you want to delete' + ) + '\n"' + proj.ProjectName + '"?', + 'Delete Project', + function () { + SnapCloud.reconnect( + 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.cloudError() + ); + } + ); + } + } else { // 'local, examples' + if (this.listField.selected) { + name = this.listField.selected.name; + this.ide.confirm( + localize( + 'Are you sure you want to delete' + ) + '\n"' + name + '"?', + 'Delete Project', + function () { + delete localStorage['-snap-project-' + name]; + myself.setSource(myself.source); // refresh list + } + ); + } + } +}; + +ProjectDialogMorph.prototype.shareProject = function () { + var myself = this, + proj = this.listField.selected, + entry = this.listField.active; + + if (proj) { + this.ide.confirm( + localize( + 'Are you sure you want to publish' + ) + '\n"' + proj.ProjectName + '"?', + 'Share Project', + function () { + myself.ide.showMessage('sharing\nproject...'); + SnapCloud.reconnect( + 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] + ); + }, + myself.ide.cloudError() + ); + } + ); + } +}; + +ProjectDialogMorph.prototype.unshareProject = function () { + var myself = this, + 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', + function () { + myself.ide.showMessage('unsharing\nproject...'); + SnapCloud.reconnect( + 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] + ); + }, + myself.ide.cloudError() + ); + } + ); + } +}; + ProjectDialogMorph.prototype.edit = function () { if (this.nameField) { this.nameField.edit(); diff --git a/snap.html b/snap.html index 26c5124..11e0095 100755 --- a/snap.html +++ b/snap.html @@ -16,9 +16,10 @@ + - + +