diff --git a/src/css/settings.css b/src/css/settings.css index 5f29fd8e..3f81be8a 100644 --- a/src/css/settings.css +++ b/src/css/settings.css @@ -211,4 +211,11 @@ font-weight: bold; color: white; font-style: normal; -} \ No newline at end of file +} + +.save-desktop-file-name { + word-wrap: break-word; + font-weight: bold; + color: white; + font-style: normal; +} diff --git a/src/js/app.js b/src/js/app.js index ef316761..38d2d3a8 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -95,6 +95,9 @@ this.localStorageService = new pskl.service.LocalStorageService(this.piskelController); this.localStorageService.init(); + this.desktopStorageService = new pskl.service.DesktopStorageService(this.piskelController); + this.desktopStorageService.init(); + this.imageUploadService = new pskl.service.ImageUploadService(); this.imageUploadService.init(); diff --git a/src/js/controller/piskel/PublicPiskelController.js b/src/js/controller/piskel/PublicPiskelController.js index 2778683a..4cd7bc5f 100644 --- a/src/js/controller/piskel/PublicPiskelController.js +++ b/src/js/controller/piskel/PublicPiskelController.js @@ -86,6 +86,14 @@ }); }; + ns.PublicPiskelController.prototype.setSavePath = function (savePath) { + this.piskelController.piskel.savePath = savePath; + }; + + ns.PublicPiskelController.prototype.getSavePath = function () { + return this.piskelController.piskel.savePath; + }; + ns.PublicPiskelController.prototype.replay = function (frame, replayData) { replayData.fn.apply(this.piskelController, replayData.args); }; diff --git a/src/js/controller/settings/ImportController.js b/src/js/controller/settings/ImportController.js index b1ded716..1a2df04f 100644 --- a/src/js/controller/settings/ImportController.js +++ b/src/js/controller/settings/ImportController.js @@ -12,11 +12,17 @@ this.addEventListener(this.hiddenFileInput, 'change', this.onFileUploadChange_); this.hiddenOpenPiskelInput = document.querySelector('[name="open-piskel-input"]'); - this.addEventListener(this.hiddenOpenPiskelInput, 'change', this.onOpenPiskelChange_); this.addEventListener('.browse-local-button', 'click', this.onBrowseLocalClick_); this.addEventListener('.file-input-button', 'click', this.onFileInputClick_); - this.addEventListener('.open-piskel-button', 'click', this.onOpenPiskelClick_); + + // different handlers, depending on the Environment + if (pskl.utils.Environment.detectNodeWebkit()) { + this.addEventListener('.open-piskel-button', 'click', this.openPiskelDesktop_); + } else { + this.addEventListener(this.hiddenOpenPiskelInput, 'change', this.onOpenPiskelChange_); + this.addEventListener('.open-piskel-button', 'click', this.onOpenPiskelClick_); + } this.initRestoreSession_(); }; @@ -55,6 +61,11 @@ } }; + ns.ImportController.prototype.openPiskelDesktop_ = function (evt) { + this.closeDrawer_(); + pskl.app.desktopStorageService.openPiskel(); + }; + ns.ImportController.prototype.onOpenPiskelClick_ = function (evt) { this.hiddenOpenPiskelInput.click(); }; diff --git a/src/js/controller/settings/SaveController.js b/src/js/controller/settings/SaveController.js index 0ca43693..e6ce11a9 100644 --- a/src/js/controller/settings/SaveController.js +++ b/src/js/controller/settings/SaveController.js @@ -29,7 +29,18 @@ this.isPublicCheckbox.setAttribute('checked', true); } - this.addEventListener('#save-file-button', 'click', this.saveFile_); + //Environment dependent configuration: + if (pskl.utils.Environment.detectNodeWebkit()) { + // running in Node-Webkit... + this.addEventListener('#save-file-button', 'click', this.saveFileDesktop); + // hide the "save in browser" part of the gui + var saveInBrowserSection = document.querySelector('#save-in-browser'); + saveInBrowserSection.style.display = 'none'; + } else { + // running in browser... + this.addEventListener('#save-file-button', 'click', this.saveFileBrowser_); + } + this.addEventListener('#save-browser-button', 'click', this.saveLocal_); this.saveOnlineButton = document.querySelector('#save-online-button'); @@ -53,9 +64,18 @@ }; ns.SaveController.prototype.updateLocalStatusFilename_ = function () { - this.saveFileStatus.innerHTML = pskl.utils.Template.getAndReplace('save-file-status-template', { - name : this.getLocalFilename_() - }); + if (pskl.utils.Environment.detectNodeWebkit()) { + var fileName = this.piskelController.getSavePath(); + if (fileName !== null) { + this.saveFileStatus.innerHTML = pskl.utils.Template.getAndReplace('save-file-status-desktop-template', { + name : this.piskelController.getSavePath() + }); + } + } else { + this.saveFileStatus.innerHTML = pskl.utils.Template.getAndReplace('save-file-status-template', { + name : this.getLocalFilename_() + }); + } }; ns.SaveController.prototype.getLocalFilename_ = function () { @@ -122,7 +142,20 @@ } }; + /** + * @deprecated - renamed "saveFileBrowser_" + * @private + */ ns.SaveController.prototype.saveFile_ = function () { + // detect if this is running in NodeWebkit + if (pskl.utils.Environment.detectNodeWebkit()) { + this.saveFileDesktop_(); + } else { + this.saveFileBrowser_(); + } + }; + + ns.SaveController.prototype.saveFileBrowser_ = function () { this.beforeSaving_(); pskl.utils.BlobUtils.stringToBlob(pskl.app.piskelController.serialize(), function(blob) { pskl.utils.FileUtils.downloadAsFile(blob, this.getLocalFilename_()); @@ -131,6 +164,10 @@ }.bind(this), "application/piskel+json"); }; + ns.SaveController.prototype.saveFileDesktop_ = function () { + pskl.app.desktopStorageService.save(); + }; + ns.SaveController.prototype.getName = function () { return this.nameInput.value; }; diff --git a/src/js/controller/settings/resize/ResizeController.js b/src/js/controller/settings/resize/ResizeController.js index 96bdcba7..7564b181 100644 --- a/src/js/controller/settings/resize/ResizeController.js +++ b/src/js/controller/settings/resize/ResizeController.js @@ -50,7 +50,10 @@ var piskel = pskl.model.Piskel.fromLayers(resizedLayers, this.piskelController.getPiskel().getDescriptor()); + // propagate savepath to new Piskel + piskel.savePath = pskl.app.piskelController.getSavePath(); pskl.app.piskelController.setPiskel(piskel, true); + $.publish(Events.CLOSE_SETTINGS_DRAWER); }; diff --git a/src/js/model/Piskel.js b/src/js/model/Piskel.js index fcc1ebdd..bbd0637b 100644 --- a/src/js/model/Piskel.js +++ b/src/js/model/Piskel.js @@ -20,6 +20,10 @@ this.height = height; this.descriptor = descriptor; + + /** @type {String} */ + this.savePath = null; + } else { throw 'Missing arguments in Piskel constructor : ' + Array.prototype.join.call(arguments, ","); } diff --git a/src/js/rendering/CompositeRenderer.js b/src/js/rendering/CompositeRenderer.js index 453a7638..ff8a2bdf 100644 --- a/src/js/rendering/CompositeRenderer.js +++ b/src/js/rendering/CompositeRenderer.js @@ -48,7 +48,6 @@ return this.getSampleRenderer_().getOffset(); }; - ns.CompositeRenderer.prototype.setGridWidth = function (b) { this.renderers.forEach(function (renderer) { renderer.setGridWidth(b); diff --git a/src/js/service/DesktopStorageService.js b/src/js/service/DesktopStorageService.js new file mode 100644 index 00000000..7aef36a7 --- /dev/null +++ b/src/js/service/DesktopStorageService.js @@ -0,0 +1,71 @@ +(function () { + var ns = $.namespace('pskl.service'); + + ns.DesktopStorageService = function(piskelController) { + this.piskelController = piskelController || pskl.app.piskelController; + this.hideNotificationTimeoutID = 0; + }; + + ns.DesktopStorageService.prototype.init = function (){ + // activate keyboard shortcuts if this is the desktop version + if (pskl.utils.Environment.detectNodeWebkit()) { + pskl.app.shortcutService.addShortcut('ctrl+o', this.openPiskel.bind(this)); + pskl.app.shortcutService.addShortcut('ctrl+s', this.save.bind(this)); + pskl.app.shortcutService.addShortcut('ctrl+shift+s', this.savePiskelAs.bind(this)); + } + }; + + ns.DesktopStorageService.prototype.save = function () { + var savePath = this.piskelController.getSavePath(); + // if we already have a filename, just save the file (using nodejs 'fs' api) + if (savePath) { + this.savePiskel(savePath); + } else { + this.savePiskelAs(savePath); + } + }; + + ns.DesktopStorageService.prototype.savePiskel = function (savePath) { + var serialized = this.piskelController.serialize(); + pskl.utils.FileUtilsDesktop.saveToFile(serialized, savePath, function () { + this.onSaveSuccess_(); + }.bind(this)); + }; + + ns.DesktopStorageService.prototype.openPiskel = function () { + pskl.utils.FileUtilsDesktop.chooseFileDialog(function(filename){ + var savePath = filename; + pskl.utils.FileUtilsDesktop.readFile(savePath, function(content){ + pskl.utils.PiskelFileUtils.decodePiskelFile(content, function (piskel, descriptor, fps) { + piskel.setDescriptor(descriptor); + // store save path so we can re-save without opening the save dialog + piskel.savePath = savePath; + pskl.app.piskelController.setPiskel(piskel); + pskl.app.animationController.setFPS(fps); + }); + }); + }); + }; + + ns.DesktopStorageService.prototype.savePiskelAs = function (savePath) { + var serialized = this.piskelController.serialize(); + // TODO: if there is already a file path, use it for the dialog's + // working directory and filename + pskl.utils.FileUtilsDesktop.saveAs(serialized, null, 'piskel', function (selectedSavePath) { + this.onSaveSuccess_(); + this.piskelController.setSavePath(selectedSavePath); + }.bind(this)); + }; + + ns.DesktopStorageService.prototype.onSaveSuccess_ = function () { + var savePath = this.piskelController.getSavePath(); + $.publish(Events.CLOSE_SETTINGS_DRAWER); + $.publish(Events.SHOW_NOTIFICATION, [{"content": "Successfully saved: " + savePath}]); + $.publish(Events.PISKEL_SAVED); + // clear the old time out, if any. + window.clearTimeout(this.hideNotificationTimeoutID); + this.hideNotificationTimeoutID = + window.setTimeout($.publish.bind($, Events.HIDE_NOTIFICATION), 3000); + }; + +})(); diff --git a/src/js/service/HistoryService.js b/src/js/service/HistoryService.js index 611e8709..b974bb0f 100644 --- a/src/js/service/HistoryService.js +++ b/src/js/service/HistoryService.js @@ -8,7 +8,6 @@ this.stateQueue = []; this.currentIndex = -1; - this.lastLoadState = -1; }; @@ -124,6 +123,8 @@ ns.HistoryService.prototype.onPiskelLoaded_ = function (index, snapshotIndex, piskel) { var originalSize = this.getPiskelSize_(); piskel.setDescriptor(this.piskelController.piskel.getDescriptor()); + // propagate save path to the new piskel instance + piskel.savePath = this.piskelController.piskel.savePath; this.piskelController.setPiskel(piskel); for (var i = snapshotIndex + 1 ; i <= index ; i++) { diff --git a/src/js/utils/Environment.js b/src/js/utils/Environment.js new file mode 100644 index 00000000..a6b6cfb0 --- /dev/null +++ b/src/js/utils/Environment.js @@ -0,0 +1,25 @@ +/** + * detection method from: + * http://videlais.com/2014/08/23/lessons-learned-from-detecting-node-webkit/ + */ + +(function () { + + var ns = $.namespace('pskl.utils'); + + ns.Environment = { + detectNodeWebkit : function () { + var isNode = (typeof window.process !== "undefined" && typeof window.require !== "undefined"); + var isNodeWebkit = false; + if (isNode) { + try { + isNodeWebkit = (typeof window.require('nw.gui') !== "undefined"); + } catch (e) { + isNodeWebkit = false; + } + } + return isNodeWebkit; + } + }; + +})(); diff --git a/src/js/utils/FileUtils.js b/src/js/utils/FileUtils.js index 4189e325..4d8a02e5 100644 --- a/src/js/utils/FileUtils.js +++ b/src/js/utils/FileUtils.js @@ -38,5 +38,6 @@ document.body.removeChild(downloadLink); } } + }; })(); diff --git a/src/js/utils/FileUtilsDesktop.js b/src/js/utils/FileUtilsDesktop.js new file mode 100644 index 00000000..6a724e1e --- /dev/null +++ b/src/js/utils/FileUtilsDesktop.js @@ -0,0 +1,83 @@ +(function () { + var ns = $.namespace('pskl.utils'); + + var stopPropagation = function (e) { + e.stopPropagation(); + }; + + ns.FileUtilsDesktop = { + + chooseFileDialog: function (callback) { + var tagString = ''; + var $chooser = $(tagString); + $chooser.change(function(e) { + var filename = $(this).val(); + callback(filename); + }); + $chooser.trigger('click'); + }, + + /** + * + * @param content + * @param defaultFileName - file name to pre-populate the dialog + * @param extension - if supplied, the selected extension will guaranteed to be on the filename - + * NOTE: there is a possible danger here... If the extension is added to a fileName, but there + * is already another file of the same name *with* the extension, it will get overwritten. + * @param callback + */ + saveAs: function (content, defaultFileName, extension, callback) { + // NodeWebkit has no js api for opening the save dialog. + // Instead, it adds two new attributes to the anchor tag: nwdirectory and nwsaveas + // (see: https://github.com/nwjs/nw.js/wiki/File-dialogs ) + defaultFileName = defaultFileName || ""; + var tagString = ''; + var $chooser = $(tagString); + $chooser.change(function(e) { + var filename = $(this).val(); + if (typeof extension == 'string') { + if (extension[0] !== '.') { + extension = "." + extension; + } + var hasExtension = (filename.substring(filename.length - extension.length) === extension); + if (!hasExtension) { + filename += extension; + } + } + pskl.utils.FileUtilsDesktop.saveToFile(content, filename, function(){ + callback(filename); + }); + }); + $chooser.trigger('click'); + }, + + /** + * Save data directly to disk, without showing a save dialog + * Requires Node-Webkit environment for file system access + * @param content - data to be saved + * @param {string} filename - fill path to the file + * @callback callback + */ + saveToFile : function(content, filename, callback) { + var fs = window.require('fs'); + fs.writeFile(filename, content, function(err){ + if (err) { + //throw err; + console.log('FileUtilsDesktop::savetoFile() - error saving file:', filename, 'Error:', err); + } + callback(); + }); + }, + + readFile : function(filename, callback) { + var fs = window.require('fs'); + // NOTE: currently loading everything as utf8, which may not be desirable in future + fs.readFile(filename, 'utf8', function(err, data){ + if (err) { + console.log('FileUtilsDesktop::readFile() - error reading file:', filename, 'Error:', err); + } + callback(data); + }); + } + }; +})(); diff --git a/src/js/utils/PiskelFileUtils.js b/src/js/utils/PiskelFileUtils.js index 59d64aed..009f1355 100644 --- a/src/js/utils/PiskelFileUtils.js +++ b/src/js/utils/PiskelFileUtils.js @@ -13,13 +13,29 @@ loadFromFile : function (file, onSuccess, onError) { pskl.utils.FileUtils.readFile(file, function (content) { var rawPiskel = pskl.utils.Base64.toText(content); - var serializedPiskel = JSON.parse(rawPiskel); - var fps = serializedPiskel.piskel.fps; - var descriptor = new pskl.model.piskel.Descriptor(serializedPiskel.piskel.name, serializedPiskel.piskel.description, true); - pskl.utils.serialization.Deserializer.deserialize(serializedPiskel, function (piskel) { - onSuccess(piskel, descriptor, fps); - }); + ns.PiskelFileUtils.decodePiskelFile( + rawPiskel, + function (piskel, descriptor, fps) { + // if using Node-Webkit, store the savePath on load + // Note: the 'path' property is unique to Node-Webkit, and holds the full path + if (pskl.utils.Environment.detectNodeWebkit()) { + piskel.savePath = file.path; + } + onSuccess(piskel, descriptor, fps); + }, + onError + ); + }); + }, + + decodePiskelFile : function (rawPiskel, onSuccess, onError) { + var serializedPiskel = JSON.parse(rawPiskel); + var fps = serializedPiskel.piskel.fps; + var descriptor = new pskl.model.piskel.Descriptor(serializedPiskel.piskel.name, serializedPiskel.piskel.description, true); + pskl.utils.serialization.Deserializer.deserialize(serializedPiskel, function (piskel) { + onSuccess(piskel, descriptor, fps); }); } + }; })(); \ No newline at end of file diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index cac6f3bc..02a73b0f 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -18,8 +18,10 @@ "js/utils/DateUtils.js", "js/utils/Dom.js", "js/utils/Event.js", + "js/utils/Environment.js", "js/utils/Math.js", "js/utils/FileUtils.js", + "js/utils/FileUtilsDesktop.js", "js/utils/FrameTransform.js", "js/utils/FrameUtils.js", "js/utils/LayerUtils.js", @@ -129,6 +131,7 @@ "js/service/LocalStorageService.js", "js/service/GithubStorageService.js", "js/service/AppEngineStorageService.js", + "js/service/DesktopStorageService.js", "js/service/BackupService.js", "js/service/BeforeUnloadService.js", "js/service/HistoryService.js", diff --git a/src/templates/settings/save.html b/src/templates/settings/save.html index d86170c3..8a34244a 100644 --- a/src/templates/settings/save.html +++ b/src/templates/settings/save.html @@ -20,10 +20,12 @@
-
Save offline in Browser
-
- -
Your piskel will be saved locally and will only be accessible from this browser.
+
+
Save offline in Browser
+
+ +
Your piskel will be saved locally and will only be accessible from this browser.
+
Save offline as File
@@ -41,6 +43,10 @@ -
\ No newline at end of file + + +