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;
color: white;
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.init();
this.desktopStorageService = new pskl.service.DesktopStorageService(this.piskelController);
this.desktopStorageService.init();
this.imageUploadService = new pskl.service.ImageUploadService();
this.imageUploadService.init();

View File

@ -136,6 +136,14 @@
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) {
$.publish(Events.PISKEL_SAVE_STATE, {
type : pskl.service.HistoryService.REPLAY_NO_SNAPSHOT,

View File

@ -17,10 +17,15 @@
this.fileInputButton.click(this.onFileInputClick_.bind(this));
this.hiddenOpenPiskelInput = $('[name=open-piskel-input]');
this.hiddenOpenPiskelInput.change(this.onOpenPiskelChange_.bind(this));
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.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) {
this.hiddenOpenPiskelInput.click();
};

View File

@ -31,7 +31,8 @@
var resizedLayers = this.piskelController.getLayers().map(this.resizeLayer_.bind(this));
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);
};

View File

@ -26,7 +26,17 @@
this.isPublicCheckbox.prop('checked', descriptor.isPublic);
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.click(this.saveLocal_.bind(this));
@ -53,9 +63,18 @@
};
ns.SaveController.prototype.updateLocalStatusFilename_ = function () {
this.saveFileStatus.html(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.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 () {
@ -122,7 +141,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 +163,10 @@
}.bind(this), "application/piskel+json");
};
ns.SaveController.prototype.saveFileDesktop_ = function () {
pskl.app.desktopStorageService.save();
};
ns.SaveController.prototype.getName = function () {
return this.nameInput.val();
};

View File

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

View File

@ -48,7 +48,6 @@
return this.getSampleRenderer_().getOffset();
};
ns.CompositeRenderer.prototype.setGridWidth = function (b) {
this.renderers.forEach(function (renderer) {
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.currentIndex = -1;
this.lastLoadState = -1;
this.saveNextAsSnapshot = false;
@ -127,6 +126,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++) {

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

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

View File

@ -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",
@ -125,6 +127,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",

View File

@ -20,10 +20,12 @@
<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>
<div class="settings-title">Save offline in Browser</div>
<div class="settings-item">
<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 id="save-in-browser">
<div class="settings-title">Save offline in Browser</div>
<div class="settings-item">
<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 class="settings-title">Save offline as File</div>
<div class="settings-item">
@ -41,6 +43,10 @@
</script>
<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>
</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>