Merge pull request #259 from leegrey/desktop-save-action

Desktop IO Features
This commit is contained in:
Julian Descottes 2015-03-26 10:42:52 +01:00
commit c600d62bd2
16 changed files with 297 additions and 23 deletions

View File

@ -208,4 +208,11 @@
font-weight: bold; font-weight: bold;
color: white; color: white;
font-style: normal; font-style: normal;
} }
.save-desktop-file-name {
word-wrap: break-word;
font-weight: bold;
color: white;
font-style: normal;
}

View File

@ -99,6 +99,9 @@
this.localStorageService = new pskl.service.LocalStorageService(this.piskelController); this.localStorageService = new pskl.service.LocalStorageService(this.piskelController);
this.localStorageService.init(); this.localStorageService.init();
this.desktopStorageService = new pskl.service.DesktopStorageService(this.piskelController);
this.desktopStorageService.init();
this.imageUploadService = new pskl.service.ImageUploadService(); this.imageUploadService = new pskl.service.ImageUploadService();
this.imageUploadService.init(); this.imageUploadService.init();

View File

@ -136,6 +136,14 @@
return this.piskelController.piskel; return this.piskelController.piskel;
}; };
ns.PublicPiskelController.prototype.setSavePath = function (savePath) {
this.piskelController.piskel.savePath = savePath;
};
ns.PublicPiskelController.prototype.getSavePath = function () {
return this.piskelController.piskel.savePath;
};
ns.PublicPiskelController.prototype.raiseSaveStateEvent_ = function (fn, args) { ns.PublicPiskelController.prototype.raiseSaveStateEvent_ = function (fn, args) {
$.publish(Events.PISKEL_SAVE_STATE, { $.publish(Events.PISKEL_SAVE_STATE, {
type : pskl.service.HistoryService.REPLAY_NO_SNAPSHOT, type : pskl.service.HistoryService.REPLAY_NO_SNAPSHOT,

View File

@ -17,10 +17,15 @@
this.fileInputButton.click(this.onFileInputClick_.bind(this)); this.fileInputButton.click(this.onFileInputClick_.bind(this));
this.hiddenOpenPiskelInput = $('[name=open-piskel-input]'); this.hiddenOpenPiskelInput = $('[name=open-piskel-input]');
this.hiddenOpenPiskelInput.change(this.onOpenPiskelChange_.bind(this));
this.openPiskelInputButton = $('.open-piskel-button'); this.openPiskelInputButton = $('.open-piskel-button');
this.openPiskelInputButton.click(this.onOpenPiskelClick_.bind(this));
// different handlers, depending on the Environment
if (pskl.utils.Environment.detectNodeWebkit()) {
this.openPiskelInputButton.click(this.openPiskelDesktop_.bind(this));
} else {
this.hiddenOpenPiskelInput.change(this.onOpenPiskelChange_.bind(this));
this.openPiskelInputButton.click(this.onOpenPiskelClick_.bind(this));
}
this.prevSessionContainer = $('.previous-session'); this.prevSessionContainer = $('.previous-session');
this.previousSessionTemplate_ = pskl.utils.Template.get("previous-session-info-template"); this.previousSessionTemplate_ = pskl.utils.Template.get("previous-session-info-template");
@ -45,6 +50,11 @@
} }
}; };
ns.ImportController.prototype.openPiskelDesktop_ = function (evt) {
this.closeDrawer_();
pskl.app.desktopStorageService.openPiskel();
};
ns.ImportController.prototype.onOpenPiskelClick_ = function (evt) { ns.ImportController.prototype.onOpenPiskelClick_ = function (evt) {
this.hiddenOpenPiskelInput.click(); this.hiddenOpenPiskelInput.click();
}; };

View File

@ -31,7 +31,8 @@
var resizedLayers = this.piskelController.getLayers().map(this.resizeLayer_.bind(this)); var resizedLayers = this.piskelController.getLayers().map(this.resizeLayer_.bind(this));
var piskel = pskl.model.Piskel.fromLayers(resizedLayers, this.piskelController.getPiskel().getDescriptor()); 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); pskl.app.piskelController.setPiskel(piskel, true);
$.publish(Events.CLOSE_SETTINGS_DRAWER); $.publish(Events.CLOSE_SETTINGS_DRAWER);
}; };

View File

@ -26,7 +26,17 @@
this.isPublicCheckbox.prop('checked', descriptor.isPublic); this.isPublicCheckbox.prop('checked', descriptor.isPublic);
this.saveFileButton = $('#save-file-button'); this.saveFileButton = $('#save-file-button');
this.saveFileButton.click(this.saveFile_.bind(this));
//Environment dependent configuration:
if (pskl.utils.Environment.detectNodeWebkit()) {
// running in Node-Webkit...
this.saveFileButton.click(this.saveFileDesktop_.bind(this));
// hide the "save in browser" part of the gui
$('#save-in-browser').css('display', 'none');
} else {
// running in browser...
this.saveFileButton.click(this.saveFileBrowser_.bind(this));
}
this.saveLocalButton = $('#save-browser-button'); this.saveLocalButton = $('#save-browser-button');
this.saveLocalButton.click(this.saveLocal_.bind(this)); this.saveLocalButton.click(this.saveLocal_.bind(this));
@ -53,9 +63,18 @@
}; };
ns.SaveController.prototype.updateLocalStatusFilename_ = function () { ns.SaveController.prototype.updateLocalStatusFilename_ = function () {
this.saveFileStatus.html(pskl.utils.Template.getAndReplace('save-file-status-template', { if (pskl.utils.Environment.detectNodeWebkit()) {
name : this.getLocalFilename_() var fileName = this.piskelController.getSavePath();
})); if (fileName !== null) {
this.saveFileStatus.html(pskl.utils.Template.getAndReplace('save-file-status-desktop-template', {
name : this.piskelController.getSavePath()
}));
}
} else {
this.saveFileStatus.html(pskl.utils.Template.getAndReplace('save-file-status-template', {
name : this.getLocalFilename_()
}));
}
}; };
ns.SaveController.prototype.getLocalFilename_ = function () { ns.SaveController.prototype.getLocalFilename_ = function () {
@ -122,7 +141,20 @@
} }
}; };
/**
* @deprecated - renamed "saveFileBrowser_"
* @private
*/
ns.SaveController.prototype.saveFile_ = function () { 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_(); this.beforeSaving_();
pskl.utils.BlobUtils.stringToBlob(pskl.app.piskelController.serialize(), function(blob) { pskl.utils.BlobUtils.stringToBlob(pskl.app.piskelController.serialize(), function(blob) {
pskl.utils.FileUtils.downloadAsFile(blob, this.getLocalFilename_()); pskl.utils.FileUtils.downloadAsFile(blob, this.getLocalFilename_());
@ -131,6 +163,10 @@
}.bind(this), "application/piskel+json"); }.bind(this), "application/piskel+json");
}; };
ns.SaveController.prototype.saveFileDesktop_ = function () {
pskl.app.desktopStorageService.save();
};
ns.SaveController.prototype.getName = function () { ns.SaveController.prototype.getName = function () {
return this.nameInput.val(); return this.nameInput.val();
}; };

View File

@ -20,6 +20,10 @@
this.height = height; this.height = height;
this.descriptor = descriptor; this.descriptor = descriptor;
/** @type {String} */
this.savePath = null;
} else { } else {
throw 'Missing arguments in Piskel constructor : ' + Array.prototype.join.call(arguments, ","); throw 'Missing arguments in Piskel constructor : ' + Array.prototype.join.call(arguments, ",");
} }

View File

@ -48,7 +48,6 @@
return this.getSampleRenderer_().getOffset(); return this.getSampleRenderer_().getOffset();
}; };
ns.CompositeRenderer.prototype.setGridWidth = function (b) { ns.CompositeRenderer.prototype.setGridWidth = function (b) {
this.renderers.forEach(function (renderer) { this.renderers.forEach(function (renderer) {
renderer.setGridWidth(b); renderer.setGridWidth(b);

View File

@ -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);
};
})();

View File

@ -8,7 +8,6 @@
this.stateQueue = []; this.stateQueue = [];
this.currentIndex = -1; this.currentIndex = -1;
this.lastLoadState = -1; this.lastLoadState = -1;
this.saveNextAsSnapshot = false; this.saveNextAsSnapshot = false;
@ -127,6 +126,8 @@
ns.HistoryService.prototype.onPiskelLoaded_ = function (index, snapshotIndex, piskel) { ns.HistoryService.prototype.onPiskelLoaded_ = function (index, snapshotIndex, piskel) {
var originalSize = this.getPiskelSize_(); var originalSize = this.getPiskelSize_();
piskel.setDescriptor(this.piskelController.piskel.getDescriptor()); piskel.setDescriptor(this.piskelController.piskel.getDescriptor());
// propagate save path to the new piskel instance
piskel.savePath = this.piskelController.piskel.savePath;
this.piskelController.setPiskel(piskel); this.piskelController.setPiskel(piskel);
for (var i = snapshotIndex + 1 ; i <= index ; i++) { for (var i = snapshotIndex + 1 ; i <= index ; i++) {

View File

@ -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;
}
};
})();

View File

@ -38,5 +38,6 @@
document.body.removeChild(downloadLink); document.body.removeChild(downloadLink);
} }
} }
}; };
})(); })();

View File

@ -0,0 +1,83 @@
(function () {
var ns = $.namespace('pskl.utils');
var stopPropagation = function (e) {
e.stopPropagation();
};
ns.FileUtilsDesktop = {
chooseFileDialog: function (callback) {
var tagString = '<input type="file" nwworkingdir=""/>';
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 = '<input type="file" nwsaveas="'+ defaultFileName +'" nwworkingdir=""/>';
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);
});
}
};
})();

View File

@ -13,13 +13,29 @@
loadFromFile : function (file, onSuccess, onError) { loadFromFile : function (file, onSuccess, onError) {
pskl.utils.FileUtils.readFile(file, function (content) { pskl.utils.FileUtils.readFile(file, function (content) {
var rawPiskel = pskl.utils.Base64.toText(content); var rawPiskel = pskl.utils.Base64.toText(content);
var serializedPiskel = JSON.parse(rawPiskel); ns.PiskelFileUtils.decodePiskelFile(
var fps = serializedPiskel.piskel.fps; rawPiskel,
var descriptor = new pskl.model.piskel.Descriptor(serializedPiskel.piskel.name, serializedPiskel.piskel.description, true); function (piskel, descriptor, fps) {
pskl.utils.serialization.Deserializer.deserialize(serializedPiskel, function (piskel) { // if using Node-Webkit, store the savePath on load
onSuccess(piskel, descriptor, fps); // 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);
}); });
} }
}; };
})(); })();

View File

@ -18,8 +18,10 @@
"js/utils/DateUtils.js", "js/utils/DateUtils.js",
"js/utils/Dom.js", "js/utils/Dom.js",
"js/utils/Event.js", "js/utils/Event.js",
"js/utils/Environment.js",
"js/utils/Math.js", "js/utils/Math.js",
"js/utils/FileUtils.js", "js/utils/FileUtils.js",
"js/utils/FileUtilsDesktop.js",
"js/utils/FrameTransform.js", "js/utils/FrameTransform.js",
"js/utils/FrameUtils.js", "js/utils/FrameUtils.js",
"js/utils/LayerUtils.js", "js/utils/LayerUtils.js",
@ -125,6 +127,7 @@
"js/service/LocalStorageService.js", "js/service/LocalStorageService.js",
"js/service/GithubStorageService.js", "js/service/GithubStorageService.js",
"js/service/AppEngineStorageService.js", "js/service/AppEngineStorageService.js",
"js/service/DesktopStorageService.js",
"js/service/BackupService.js", "js/service/BackupService.js",
"js/service/BeforeUnloadService.js", "js/service/BeforeUnloadService.js",
"js/service/HistoryService.js", "js/service/HistoryService.js",

View File

@ -20,10 +20,12 @@
<input type="button" class="button button-primary" id="save-online-button" value="Save to your gallery" /> <input type="button" class="button button-primary" id="save-online-button" value="Save to your gallery" />
<div id="save-online-status" class="save-status"></div> <div id="save-online-status" class="save-status"></div>
</div> </div>
<div class="settings-title">Save offline in Browser</div> <div id="save-in-browser">
<div class="settings-item"> <div class="settings-title">Save offline in Browser</div>
<input type="button" class="button button-primary" id="save-browser-button" value="Save in Browser" /> <div class="settings-item">
<div id="save-browser-status" class="save-status">Your piskel will be saved locally and will only be accessible from this browser.</div> <input type="button" class="button button-primary" id="save-browser-button" value="Save in Browser" />
<div id="save-browser-status" class="save-status">Your piskel will be saved locally and will only be accessible from this browser.</div>
</div>
</div> </div>
<div class="settings-title">Save offline as File</div> <div class="settings-title">Save offline as File</div>
<div class="settings-item"> <div class="settings-item">
@ -41,6 +43,10 @@
</script> </script>
<script type="text/template" id="save-file-status-template"> <script type="text/template" id="save-file-status-template">
<span>Your piskel will be downloaded as <span class="save-file-name">{{name}}<span></span> <span>Your piskel will be downloaded as: <span class="save-file-name">{{name}}<span></span>
</script> </script>
</div>
<script type="text/template" id="save-file-status-desktop-template">
<span>Saving as: <span class="save-desktop-file-name">{{name}}<span></span>
</script>
</div>