42 Commits

Author SHA1 Message Date
juliandescottes
43f4f3a86e Issue #711 : WIP move cursor with keyboard 2017-09-24 18:22:56 +02:00
juliandescottes
eb27c82628 chore: add comment to onKeyUp method in DrawingController 2017-09-24 17:43:46 +02:00
Julian Descottes
da739e78da Issue #743 - bump color palette cap to 256 2017-09-24 17:39:03 +02:00
Julian Descottes
dd8217e21b Issue #744 - show notification when exporting to GIF can not preserve colors 2017-09-24 17:37:49 +02:00
Julian Descottes
d502d3416b Issue #745 - Add https support 2017-09-24 17:37:14 +02:00
juliandescottes
d1156954ca Issue #729 - implement custom PNG export viewer instead of opening window to data-uri 2017-09-24 17:36:02 +02:00
juliandescottes
dc5209628c fix selectionmanager unit test 2017-09-06 23:05:17 +02:00
juliandescottes
8568663949 Move clipboard events to dedicated service and fix tests 2017-09-06 23:05:17 +02:00
juliandescottes
fd3d828067 remove unused selection copy cut paste events 2017-09-06 23:05:17 +02:00
juliandescottes
e1797b2008 Fix SelectionManagerTest by using a clipboard event mock 2017-09-06 23:05:17 +02:00
juliandescottes
0a43f6bbec Fix copy to website script to work if main-partial is missing. 2017-09-06 23:05:17 +02:00
juliandescottes
b9423bc831 Issue #645: Support clipboard to paste images 2017-09-06 23:05:17 +02:00
juliandescottes
5e6280301d Issue #736 - cleanup selection tool state on SELECTION_DISMISSED event 2017-09-06 00:39:35 +02:00
juliandescottes
5671eb4782 Delete all extra backup sessions if MAX is reached 2017-08-06 22:56:43 +02:00
juliandescottes
35788b54ba update travis yml to upgrade node and stop downloading casper 2017-08-03 00:44:53 +02:00
juliandescottes
629ecf83b4 add comments for values synced between JS and CSS 2017-08-03 00:21:08 +02:00
juliandescottes
c037b07693 rename mergeData to backupsData in browse backups wizard 2017-08-03 00:21:08 +02:00
juliandescottes
c31b7a351c update piskel mock in BackupServiceTest 2017-08-03 00:21:08 +02:00
juliandescottes
7de03f1e73 show snpashot previews in the browse backups dialog 2017-08-03 00:21:08 +02:00
juliandescottes
eab21e0839 Show confirmation message when loading snapshot backup 2017-08-03 00:21:08 +02:00
juliandescottes
2b3bd02479 improve styling of snapshot list in browse backups dialog 2017-08-03 00:21:08 +02:00
juliandescottes
4e86fa1570 dev-environment: add ctrl+alt+R shortcut to reload styles 2017-08-03 00:21:08 +02:00
juliandescottes
170a7e4731 skip backups for current session in browse backups dialog 2017-08-03 00:21:08 +02:00
juliandescottes
6b7f04b63e browse backups dialog: add styling for empty session list 2017-08-03 00:21:08 +02:00
juliandescottes
da2e9f99e4 cleanup: remove title on backup session element 2017-08-03 00:21:08 +02:00
juliandescottes
530a949e54 add icon for backup dialog 2017-08-03 00:21:08 +02:00
Julian Descottes
4377c9e601 add disclaimer in the browse backups dialog 2017-08-03 00:21:08 +02:00
Julian Descottes
e0bbb88d47 confirm backup session delete, add animation 2017-08-03 00:21:08 +02:00
Julian Descottes
9ff2ecbb45 improve styling for browse-backups dialog 2017-08-03 00:21:08 +02:00
juliandescottes
8beba2088b remove useless console.log 2017-08-03 00:21:08 +02:00
juliandescottes
ee45cdcc45 add a browse backups dialog 2017-08-03 00:21:08 +02:00
juliandescottes
30ea7fa079 fix migration script for localstorage to indexeddb 2017-08-03 00:21:08 +02:00
Julian Descottes
e9b39a5c61 add unit test for PiskelDatabase 2017-08-03 00:21:08 +02:00
Julian Descottes
d0a32b18c5 add unit test for backup database 2017-08-03 00:21:08 +02:00
Julian Descottes
372ad1f513 add unit test for BackupService 2017-08-03 00:21:08 +02:00
Julian Descottes
c6e106fe2d add a limit to the number of sessions backed up 2017-08-03 00:21:08 +02:00
Julian Descottes
f9570ea3c5 Issue #640 - extract database code to dedicated package 2017-08-03 00:21:08 +02:00
Julian Descottes
f9cb631acb Issue #640 - migrate backup service to indexeddb 2017-08-03 00:21:08 +02:00
Julian Descottes
ed749a747f Issue #640 - migrate local browser save to indexeddb 2017-08-03 00:21:08 +02:00
Julian Descottes
30ecd41452 Issue #640 - remove duplicated entries in piskel-script-list 2017-08-03 00:21:08 +02:00
Julian Descottes
af65344c23 Issue #640 - rename PaletteService pointer to localStorage to localStorageGlobal
PaletteService exposes window.localStorage as this.localStorageService. This is confusing since we also have the LocalStorageService class used to save piskels in local storage.
2017-08-03 00:21:08 +02:00
juliandescottes
183133496e Fix #718 - when dropping image, only use import wizard for big images 2017-08-01 01:06:09 +02:00
60 changed files with 4011 additions and 184 deletions

View File

@@ -1,14 +1,9 @@
language: node_js
node_js:
- "4.1"
- "7.4.0"
before_install:
- npm update -g npm
- npm install -g grunt-cli
- git clone git://github.com/n1k0/casperjs.git ~/casperjs
- cd ~/casperjs
- git checkout tags/1.1.3
- export PATH=$PATH:`pwd`/bin
- cd -
before_script:
- phantomjs --version
- casperjs --version

View File

@@ -17,7 +17,16 @@ function onCopy(err) {
console.log('Copied static files to piskel-website...');
let previousPartialPath = path.resolve(PISKELAPP_PATH, 'templates/editor/main-partial.html');
fs.unlink(previousPartialPath, onDeletePreviousPartial);
fs.access(previousPartialPath, fs.constants.F_OK, function (err) {
if (err) {
// File does not exit, call next step directly.
console.error('Previous main partial doesn\'t exist yet.');
onDeletePreviousPartial();
} else {
// File exists, try to delete it before moving on.
fs.unlink(previousPartialPath, onDeletePreviousPartial);
}
})
}
function onDeletePreviousPartial(err) {

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="90"
height="90"
viewBox="0 0 89.999997 90"
enable-background="new 0 0 89.231 100"
xml:space="preserve"
inkscape:version="0.92.1 r15371"
sodipodi:docname="common-backup.svg"
inkscape:export-filename="C:\Development\git\piskel\misc\icons\source\tool-rotate.png"
inkscape:export-xdpi="45"
inkscape:export-ydpi="45"><metadata
id="metadata15"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs13" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1148"
id="namedview11"
showgrid="false"
inkscape:zoom="7.75"
inkscape:cx="13.031976"
inkscape:cy="43.272537"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><g
id="g3760"
transform="matrix(0,-0.97677741,0.97203982,0,-2.1261998,91.355253)"
style="fill:#ff00ff;fill-opacity:1"><path
style="fill:#ff00ff;fill-opacity:1"
inkscape:connector-curvature="0"
id="path3"
d="m 29.229405,55.37008 c -0.387431,-1.333059 -0.642506,-2.72161 -0.738881,-4.152895 h -8.675106 c 0.115651,2.460738 0.552554,4.838559 1.260594,7.099021 z" /><path
style="fill:#ff00ff;fill-opacity:1"
inkscape:connector-curvature="0"
id="path5"
d="m 29.023802,70.783821 5.579515,-6.601516 c -1.862622,-1.780814 -3.387929,-3.907969 -4.44999,-6.287065 l -8.152106,2.946124 c 1.604978,3.800815 4.017584,7.185766 7.022581,9.942457 z" /><path
style="fill:#ff00ff;fill-opacity:1"
inkscape:connector-curvature="0"
id="path7"
d="m 47.110967,69.703978 c -3.887799,-0.260871 -7.469766,-1.6322 -10.437498,-3.790608 l -5.577588,6.598963 c 4.487901,3.403448 10.011517,5.524225 16.015086,5.803594 z" /><path
style="fill:#ff00ff;fill-opacity:1;stroke:none"
inkscape:connector-curvature="0"
id="path9"
d="M 48.464084,21.400659 V 14.532532 L 28.981398,25.698341 48.464084,36.86415 v -6.867489 c 11.042093,0 20.024317,8.91683 20.024317,19.877897 0,10.509484 -8.258763,19.134189 -18.671845,19.828145 v 8.611948 c 15.190751,-0.703524 27.330245,-13.189635 27.330245,-28.440093 0,-15.700763 -12.86681,-28.473899 -28.682717,-28.473899 z" /></g><g
id="g4513"
transform="translate(0,-2)"><rect
y="32.516129"
x="42"
height="15.612903"
width="7.9999986"
id="rect4494"
style="fill:#ff00f7;fill-opacity:1;stroke:#ffffed;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:6.04534006;stroke-opacity:1" /><rect
transform="rotate(120)"
y="-76.050484"
x="12.680965"
height="15.612903"
width="7.9999986"
id="rect4494-7"
style="fill:#ff00f7;fill-opacity:1;stroke:#ffffed;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:6.04534006;stroke-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,144 @@
#dialog-container.browse-backups {
width: 700px;
height: 500px;
top : 50%;
left : 50%;
position : absolute;
margin-left: -350px;
}
.backups-step-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.backups-step-content {
width: 100%;
height: 10px;
flex-grow: 1;
background: #444;
padding: 20px;
overflow: auto;
box-sizing: border-box;
}
.backups-step-actions {
flex-grow: 0;
flex-shrink: 1;
height: 60px;
display: flex;
align-items: center;
padding: 0 20px;
}
.show #dialog-container.browse-backups {
margin-top: -250px;
}
.browse-backups .browse-backups-disclaimer {
display: flex;
margin-bottom: 20px;
align-items: center;
}
.browse-backups .browse-backups-disclaimer-content {
padding: 0 20px;
font-size: 13px;
}
.browse-backups .browse-backups-disclaimer .backups-icon {
border: 1px solid gold;
flex-shrink: 0;
width: 90px;
height: 90px;
}
.browse-backups .session-list-empty,
.browse-backups .snapshot-list-empty {
position: absolute;
left: 50%;
width: 200px;
margin-top: 100px;
margin-left: -130px;
padding: 30px;
font-size: 16px;
text-align: center;
border: 1px solid;
color: #bbb;
}
.browse-backups .session-item {
/* Transition duration should be kept in sync with SelectSession.DELETE_TRANSITION_DURATION */
transition: all 500ms;
}
/* Hide and slide up next sessions when deleting an item */
.browse-backups .session-item.deleting {
opacity: 0;
margin-bottom: -60px;
}
.browse-backups .session-item,
.browse-backups .snapshot-item {
display: flex;
align-items: center;
width: 100%;
height: 80px;
margin-bottom: 10px;
padding: 0 20px;
border: 1px solid #666;
box-sizing: border-box;
}
.browse-backups .snapshot-preview {
flex-grow: 0;
flex-shrink: 1;
/* Keep synced with SessionDetails.PREVIEW_SIZE */
height: 60px;
width: 60px;
margin-right: 20px;
}
.browse-backups .session-details,
.browse-backups .snapshot-details {
flex-grow: 1;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.browse-backups .session-details-title,
.browse-backups .snapshot-details-title {
font-size: 13px;
}
.browse-backups .session-details-info,
.browse-backups .snapshot-details-info {
font-size: 11px;
color: #bbb;
line-height: 1.5em;
}
.browse-backups .session-actions,
.browse-backups .snapshot-actions {
flex-grow: 0;
flex-shrink: 1;
display: flex;
}
.browse-backups .session-actions button,
.browse-backups .snapshot-actions button {
margin-left: 10px;
}
.browse-backups .session-item:last-child,
.browse-backups .snapshot-item:last-child {
margin-bottom: 0;
}

View File

@@ -43,8 +43,29 @@
float : left;
}
.gif-export-warning {
display: none;
}
.gif-export-warning.visible {
display: flex;
align-items: center;
border: 1px solid red;
padding: 5px;
margin: 5px 0;
}
.gif-export-warning-icon {
flex-shrink: 0;
margin-right: 5px;
}
.gif-export-warning-message {
font-weight: normal;
}
.preview-upload-ongoing:before{
content: "Upload ongoing ...";
content: "Upload in progress...";
position: absolute;
display: block;
height: 100%;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -67,6 +67,7 @@
</div>
@@include('templates/misc-templates.html', {})
@@include('templates/data-uri-export.html', {})
@@include('templates/popup-preview.html', {})
<span class="cheatsheet-link icon-common-keyboard-gold"
@@ -75,9 +76,10 @@
rel="tooltip" data-placement="left" title="Performance problem detected, learn more.">&nbsp;</div>
<!-- dialogs partials -->
@@include('templates/dialogs/create-palette.html', {})
@@include('templates/dialogs/browse-backups.html', {})
@@include('templates/dialogs/browse-local.html', {})
@@include('templates/dialogs/cheatsheet.html', {})
@@include('templates/dialogs/create-palette.html', {})
@@include('templates/dialogs/import.html', {})
@@include('templates/dialogs/performance-info.html', {})
@@include('templates/dialogs/unsupported-browser.html', {})

View File

@@ -12,7 +12,7 @@ var Constants = {
MAX_HEIGHT : 1024,
MAX_WIDTH : 1024,
MAX_PALETTE_COLORS : 100,
MAX_PALETTE_COLORS : 256,
// allow current colors service to get up to 256 colors.
// GIF generation is different if the color count goes over 256.
MAX_WORKER_COLORS : 256,
@@ -58,8 +58,11 @@ var Constants = {
// The datastore limit is 1 MiB, which we roughly approximate to 1 million characters.
APPENGINE_SAVE_LIMIT : 1 * 1024 * 1024,
// Message displayed when an action will lead to erase the current animation.
CONFIRM_OVERWRITE: 'This will replace your current animation, are you sure you want to continue?',
// SERVICE URLS
APPENGINE_SAVE_URL : 'save',
IMAGE_SERVICE_UPLOAD_URL : 'http://piskel-imgstore-b.appspot.com/__/upload',
IMAGE_SERVICE_GET_URL : 'http://piskel-imgstore-b.appspot.com/img/'
IMAGE_SERVICE_UPLOAD_URL : '{{protocol}}://piskel-imgstore-b.appspot.com/__/upload',
IMAGE_SERVICE_GET_URL : '{{protocol}}://piskel-imgstore-b.appspot.com/img/'
};

View File

@@ -64,9 +64,10 @@ var Events = {
SELECTION_CREATED: 'SELECTION_CREATED',
SELECTION_MOVE_REQUEST: 'SELECTION_MOVE_REQUEST',
SELECTION_DISMISSED: 'SELECTION_DISMISSED',
SELECTION_COPY: 'SELECTION_COPY',
SELECTION_CUT: 'SELECTION_CUT',
SELECTION_PASTE: 'SELECTION_PASTE',
CLIPBOARD_COPY: 'CLIPBOARD_COPY',
CLIPBOARD_CUT: 'CLIPBOARD_CUT',
CLIPBOARD_PASTE: 'CLIPBOARD_PASTE',
SHOW_NOTIFICATION: 'SHOW_NOTIFICATION',
HIDE_NOTIFICATION: 'HIDE_NOTIFICATION',

View File

@@ -18,9 +18,15 @@
*/
this.isAppEngineVersion = !!pskl.appEngineToken_;
// This id is used to keep track of sessions in the BackupService.
this.sessionId = pskl.utils.Uuid.generate();
this.shortcutService = new pskl.service.keyboard.ShortcutService();
this.shortcutService.init();
this.inputService = new pskl.service.keyboard.InputService();
this.inputService.init();
var size = pskl.UserSettings.get(pskl.UserSettings.DEFAULT_SIZE);
var fps = Constants.DEFAULT.FPS;
var descriptor = new pskl.model.piskel.Descriptor('New Piskel', '');
@@ -114,6 +120,9 @@
this.canvasBackgroundController = new pskl.controller.CanvasBackgroundController();
this.canvasBackgroundController.init();
this.indexedDbStorageService = new pskl.service.storage.IndexedDbStorageService(this.piskelController);
this.indexedDbStorageService.init();
this.localStorageService = new pskl.service.storage.LocalStorageService(this.piskelController);
this.localStorageService.init();
@@ -168,6 +177,9 @@
this.currentColorsService);
this.performanceReportService.init();
this.clipboardService = new pskl.service.ClipboardService(this.piskelController);
this.clipboardService.init();
this.drawingLoop = new pskl.rendering.DrawingLoop();
this.drawingLoop.addCallback(this.render, this);
this.drawingLoop.start();
@@ -195,6 +207,11 @@
dialogId : 'unsupported-browser'
});
}
if (pskl.utils.Environment.isDebug()) {
pskl.app.shortcutService.registerShortcut(pskl.service.keyboard.Shortcuts.DEBUG.RELOAD_STYLES,
window.reloadStyles);
}
},
loadPiskel_ : function (piskelData) {

View File

@@ -44,6 +44,7 @@
// State of drawing controller:
this.isClicked = false;
this.previousMousemoveTime = 0;
this.lastUpdateInputs_ = 0;
this.currentToolBehavior = null;
};
@@ -64,11 +65,17 @@
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.RESET_ZOOM, this.resetZoom_.bind(this));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.INCREASE_ZOOM, this.updateZoom_.bind(this, 1));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.DECREASE_ZOOM, this.updateZoom_.bind(this, -1));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.OFFSET_UP, this.updateOffset_.bind(this, 'up'));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.OFFSET_RIGHT, this.updateOffset_.bind(this, 'right'));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.OFFSET_DOWN, this.updateOffset_.bind(this, 'down'));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.OFFSET_LEFT, this.updateOffset_.bind(this, 'left'));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.CURSOR_UP, this.updateCursor_.bind(this, 'up'));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.CURSOR_RIGHT, this.updateCursor_.bind(this, 'right'));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.CURSOR_DOWN, this.updateCursor_.bind(this, 'down'));
pskl.app.shortcutService.registerShortcut(shortcuts.MISC.CURSOR_LEFT, this.updateCursor_.bind(this, 'left'));
window.setTimeout(function () {
this.afterWindowResize_();
this.resetZoom_();
@@ -156,6 +163,10 @@
* @private
*/
ns.DrawingController.prototype.onMousedown_ = function (event) {
if (this.isClicked) {
return;
}
$.publish(Events.MOUSE_EVENT, [event, this]);
var frame = this.piskelController.getCurrentFrame();
var coords = this.getSpriteCoordinates(event.clientX, event.clientY);
@@ -204,6 +215,7 @@
};
/**
* Trigger tool move on key up in order to acknowledge modifier changes.
* @private
*/
ns.DrawingController.prototype.onKeyup_ = function (event) {
@@ -289,6 +301,83 @@
);
};
/**
* Update the current viewport offset of 1 pixel in the provided direction.
* Direction can be one of 'up', 'right', 'down', 'left'.
* Callback for the OFFSET_${DIR} shortcuts.
*/
ns.DrawingController.prototype.updateCursor_ = function (dir) {
var x = this.currentX || 0;
var y = this.currentY || 0;
if (dir === 'up') {
y -= 1;
} else if (dir === 'right') {
x += 1;
} else if (dir === 'down') {
y += 1;
} else if (dir === 'left') {
x -= 1;
}
this.currentX = x;
this.currentY = y;
var screenCoordinates = this.getScreenCoordinates(x, y);
var event = {
'type': 'mousemove',
'button': 0,
'shiftKey': false,
'altKey': false,
'ctrlKey': false
};
event.clientX = screenCoordinates.x;
event.clientY = screenCoordinates.y;
this.onMousemove_(event);
};
ns.DrawingController.prototype.clickCursor_ = function () {
var x = this.currentX || 0;
var y = this.currentY || 0;
var screenCoordinates = this.getScreenCoordinates(x, y);
var event = {
'type': 'mousedown',
'button': 0,
'shiftKey': false,
'altKey': false,
'ctrlKey': false
};
event.clientX = screenCoordinates.x;
event.clientY = screenCoordinates.y;
this.onMousedown_(event);
};
ns.DrawingController.prototype.releaseCursor_ = function () {
var x = this.currentX || 0;
var y = this.currentY || 0;
var screenCoordinates = this.getScreenCoordinates(x, y);
var event = {
'type': 'mouseup',
'button': 0,
'shiftKey': false,
'altKey': false,
'ctrlKey': false
};
event.clientX = screenCoordinates.x;
event.clientY = screenCoordinates.y;
this.onMouseup_(event);
};
/**
* Update the current zoom level by a given multiplier.
*
@@ -428,6 +517,20 @@
this.renderer.render(currentFrame);
this.overlayRenderer.render(this.overlayFrame);
this.updateInputs_();
};
ns.DrawingController.prototype.updateInputs_ = function () {
var shortcuts = pskl.service.keyboard.Shortcuts;
if (pskl.app.inputService.isKeyPressed(shortcuts.MISC.CURSOR_CLICK)) {
this.clickCursor_();
this.isClickingCursor_ = true;
} else if (this.isClickingCursor_) {
this.releaseCursor_();
this.isClickingCursor_ = false;
}
};
/**

View File

@@ -10,7 +10,7 @@
this.localStorageItemTemplate_ = pskl.utils.Template.get('local-storage-item-template');
this.service_ = pskl.app.localStorageService;
this.service_ = pskl.app.indexedDbStorageService;
this.piskelList = $('.local-piskel-list');
this.prevSessionContainer = $('.previous-session');
@@ -36,24 +36,24 @@
};
ns.BrowseLocalController.prototype.fillLocalPiskelsList_ = function () {
var html = '';
var keys = this.service_.getKeys();
keys.sort(function (k1, k2) {
if (k1.date < k2.date) {return 1;}
if (k1.date > k2.date) {return -1;}
return 0;
});
keys.forEach((function (key) {
var date = pskl.utils.DateUtils.format(key.date, '{{Y}}/{{M}}/{{D}} {{H}}:{{m}}');
html += pskl.utils.Template.replace(this.localStorageItemTemplate_, {
name : key.name,
date : date
this.service_.getKeys().then(function (keys) {
var html = '';
keys.sort(function (k1, k2) {
if (k1.date < k2.date) {return 1;}
if (k1.date > k2.date) {return -1;}
return 0;
});
}).bind(this));
var tableBody_ = this.piskelList.get(0).tBodies[0];
tableBody_.innerHTML = html;
keys.forEach((function (key) {
var date = pskl.utils.DateUtils.format(key.date, '{{Y}}/{{M}}/{{D}} {{H}}:{{m}}');
html += pskl.utils.Template.replace(this.localStorageItemTemplate_, {
name : key.name,
date : date
});
}).bind(this));
var tableBody_ = this.piskelList.get(0).tBodies[0];
tableBody_.innerHTML = html;
}.bind(this));
};
})();

View File

@@ -25,6 +25,10 @@
'unsupported-browser' : {
template : 'templates/dialogs/unsupported-browser.html',
controller : ns.UnsupportedBrowserController
},
'browse-backups' : {
template : 'templates/dialogs/browse-backups.html',
controller : ns.backups.BrowseBackups
}
};

View File

@@ -0,0 +1,81 @@
(function () {
var ns = $.namespace('pskl.controller.dialogs.backups');
var stepDefinitions = {
'SELECT_SESSION' : {
controller : ns.steps.SelectSession,
template : 'backups-select-session'
},
'SESSION_DETAILS' : {
controller : ns.steps.SessionDetails,
template : 'backups-session-details'
},
};
ns.BrowseBackups = function (piskelController, args) {
this.piskelController = piskelController;
// Backups data object used by steps to communicate and share their
// results.
this.backupsData = {
sessions: [],
selectedSession : null
};
};
pskl.utils.inherit(ns.BrowseBackups, pskl.controller.dialogs.AbstractDialogController);
ns.BrowseBackups.prototype.init = function () {
this.superclass.init.call(this);
// Prepare wizard steps.
this.steps = this.createSteps_();
// Start wizard widget.
var wizardContainer = document.querySelector('.backups-wizard-container');
this.wizard = new pskl.widgets.Wizard(this.steps, wizardContainer);
this.wizard.init();
this.wizard.goTo('SELECT_SESSION');
};
ns.BrowseBackups.prototype.back = function () {
this.wizard.back();
this.wizard.getCurrentStep().instance.onShow();
};
ns.BrowseBackups.prototype.next = function () {
var step = this.wizard.getCurrentStep();
if (step.name === 'SELECT_SESSION') {
this.wizard.goTo('SESSION_DETAILS');
}
};
ns.BrowseBackups.prototype.destroy = function (file) {
Object.keys(this.steps).forEach(function (stepName) {
var step = this.steps[stepName];
step.instance.destroy();
step.instance = null;
step.el = null;
}.bind(this));
this.superclass.destroy.call(this);
};
ns.BrowseBackups.prototype.createSteps_ = function () {
var steps = {};
Object.keys(stepDefinitions).forEach(function (stepName) {
var definition = stepDefinitions[stepName];
var el = pskl.utils.Template.getAsHTML(definition.template);
var instance = new definition.controller(this.piskelController, this, el);
instance.init();
steps[stepName] = {
name: stepName,
el: el,
instance: instance
};
}.bind(this));
return steps;
};
})();

View File

@@ -0,0 +1,96 @@
(function () {
var ns = $.namespace('pskl.controller.dialogs.backups.steps');
// Should match the transition duration for.session-item defined in dialogs-browse-backups.css
var DELETE_TRANSITION_DURATION = 500;
/**
* Helper that returns a promise that will resolve after waiting for a
* given time (in ms).
*
* @param {Number} time
* The time to wait.
* @return {Promise} promise that resolves after time.
*/
var wait = function (time) {
var deferred = Q.defer();
setTimeout(function () {
deferred.resolve();
}, time);
return deferred.promise;
};
ns.SelectSession = function (piskelController, backupsController, container) {
this.piskelController = piskelController;
this.backupsController = backupsController;
this.container = container;
};
ns.SelectSession.prototype.addEventListener = function (el, type, cb) {
pskl.utils.Event.addEventListener(el, type, cb, this);
};
ns.SelectSession.prototype.init = function () {
this.addEventListener(this.container, 'click', this.onContainerClick_);
};
ns.SelectSession.prototype.onShow = function () {
this.update();
};
ns.SelectSession.prototype.update = function () {
pskl.app.backupService.list().then(function (sessions) {
var html = '';
if (sessions.length === 0) {
html = pskl.utils.Template.get('session-list-empty');
} else {
var sessionItemTemplate = pskl.utils.Template.get('session-list-item');
var html = '';
sessions.forEach(function (session) {
if (session.id === pskl.app.sessionId) {
// Do not show backups for the current session.
return;
}
var view = {
id: session.id,
name: session.name,
description: session.description ? '- ' + session.description : '',
date: pskl.utils.DateUtils.format(session.endDate, 'the {{Y}}/{{M}}/{{D}} at {{H}}:{{m}}'),
count: session.count === 1 ? '1 snapshot' : session.count + ' snapshots'
};
html += pskl.utils.Template.replace(sessionItemTemplate, view);
});
}
this.container.querySelector('.session-list').innerHTML = html;
}.bind(this));
};
ns.SelectSession.prototype.destroy = function () {
pskl.utils.Event.removeAllEventListeners(this);
};
ns.SelectSession.prototype.onContainerClick_ = function (evt) {
var sessionId = evt.target.dataset.sessionId;
if (!sessionId) {
return;
}
var action = evt.target.dataset.action;
if (action == 'view') {
this.backupsController.backupsData.selectedSession = sessionId;
this.backupsController.next();
} else if (action == 'delete') {
if (window.confirm('Are you sure you want to delete this session?')) {
evt.target.closest('.session-item').classList.add('deleting');
Q.all([
pskl.app.backupService.deleteSession(sessionId),
// Wait for 500ms for the .hide opacity transition.
wait(DELETE_TRANSITION_DURATION)
]).then(function () {
// Refresh the list of sessions
this.update();
}.bind(this));
}
}
};
})();

View File

@@ -0,0 +1,93 @@
(function () {
var ns = $.namespace('pskl.controller.dialogs.backups.steps');
// Should match the preview dimensions defined in dialogs-browse-backups.css
var PREVIEW_SIZE = 60;
ns.SessionDetails = function (piskelController, backupsController, container) {
this.piskelController = piskelController;
this.backupsController = backupsController;
this.container = container;
};
ns.SessionDetails.prototype.init = function () {
this.backButton = this.container.querySelector('.back-button');
this.addEventListener(this.backButton, 'click', this.onBackClick_);
this.addEventListener(this.container, 'click', this.onContainerClick_);
};
ns.SessionDetails.prototype.destroy = function () {
pskl.utils.Event.removeAllEventListeners(this);
};
ns.SessionDetails.prototype.addEventListener = function (el, type, cb) {
pskl.utils.Event.addEventListener(el, type, cb, this);
};
ns.SessionDetails.prototype.onShow = function () {
var sessionId = this.backupsController.backupsData.selectedSession;
pskl.app.backupService.getSnapshotsBySessionId(sessionId).then(function (snapshots) {
var html = '';
if (snapshots.length === 0) {
// This should normally never happen, all sessions have at least one snapshot and snapshots
// can not be individually deleted.
console.warn('Could not retrieve snapshots for a session');
html = pskl.utils.Template.get('snapshot-list-empty');
} else {
var sessionItemTemplate = pskl.utils.Template.get('snapshot-list-item');
var html = '';
snapshots.forEach(function (snapshot) {
var view = {
id: snapshot.id,
name: snapshot.name,
description: snapshot.description ? '- ' + snapshot.description : '',
date: pskl.utils.DateUtils.format(snapshot.date, 'the {{Y}}/{{M}}/{{D}} at {{H}}:{{m}}'),
frames: snapshot.frames === 1 ? '1 frame' : snapshot.frames + ' frames',
resolution: pskl.utils.StringUtils.formatSize(snapshot.width, snapshot.height),
fps: snapshot.fps
};
html += pskl.utils.Template.replace(sessionItemTemplate, view);
this.updateSnapshotPreview_(snapshot);
}.bind(this));
}
this.container.querySelector('.snapshot-list').innerHTML = html;
}.bind(this));
};
ns.SessionDetails.prototype.updateSnapshotPreview_ = function (snapshot) {
pskl.utils.serialization.Deserializer.deserialize(
JSON.parse(snapshot.serialized),
function (piskel) {
var selector = '.snapshot-item[data-snapshot-id="' + snapshot.id + '"] .snapshot-preview';
var previewContainer = this.container.querySelector(selector);
if (!previewContainer) {
return;
}
var image = this.getFirstFrameAsImage_(piskel);
previewContainer.appendChild(image);
}.bind(this)
);
};
ns.SessionDetails.prototype.getFirstFrameAsImage_ = function (piskel) {
var frame = pskl.utils.LayerUtils.mergeFrameAt(piskel.getLayers(), 0);
var wZoom = PREVIEW_SIZE / piskel.width;
var hZoom = PREVIEW_SIZE / piskel.height;
var zoom = Math.min(hZoom, wZoom);
return pskl.utils.FrameUtils.toImage(frame, zoom);
};
ns.SessionDetails.prototype.onBackClick_ = function () {
this.backupsController.back(this);
};
ns.SessionDetails.prototype.onContainerClick_ = function (evt) {
var action = evt.target.dataset.action;
if (action == 'load' && window.confirm(Constants.CONFIRM_OVERWRITE)) {
var snapshotId = evt.target.dataset.snapshotId * 1;
pskl.app.backupService.loadSnapshotById(snapshotId).then(function () {
$.publish(Events.DIALOG_HIDE);
});
}
};
})();

View File

@@ -144,9 +144,7 @@
if (mode === ns.steps.SelectMode.MODES.REPLACE) {
// Replace the current piskel and close the dialog.
var message = 'This will replace your current animation,' +
' are you sure you want to continue?';
if (window.confirm(message)) {
if (window.confirm(Constants.CONFIRM_OVERWRITE)) {
this.piskelController.setPiskel(piskel);
this.closeDialog();
}

View File

@@ -14,6 +14,7 @@
this.hiddenOpenPiskelInput = document.querySelector('[name="open-piskel-input"]');
this.addEventListener('.browse-local-button', 'click', this.onBrowseLocalClick_);
this.addEventListener('.browse-backups-button', 'click', this.onBrowseBackupsClick_);
this.addEventListener('.file-input-button', 'click', this.onFileInputClick_);
// different handlers, depending on the Environment
@@ -23,24 +24,6 @@
this.addEventListener(this.hiddenOpenPiskelInput, 'change', this.onOpenPiskelChange_);
this.addEventListener('.open-piskel-button', 'click', this.onOpenPiskelClick_);
}
this.initRestoreSession_();
};
ns.ImportController.prototype.initRestoreSession_ = function () {
var previousSessionContainer = document.querySelector('.previous-session');
var previousInfo = pskl.app.backupService.getPreviousPiskelInfo();
if (previousInfo) {
var previousSessionTemplate_ = pskl.utils.Template.get('previous-session-info-template');
var date = pskl.utils.DateUtils.format(previousInfo.date, '{{H}}:{{m}} - {{Y}}/{{M}}/{{D}}');
previousSessionContainer.innerHTML = pskl.utils.Template.replace(previousSessionTemplate_, {
name : previousInfo.name,
date : date
});
this.addEventListener('.restore-session-button', 'click', this.onRestorePreviousSessionClick_);
} else {
previousSessionContainer.innerHTML = 'No piskel backup was found on this browser.';
}
};
ns.ImportController.prototype.closeDrawer_ = function () {
@@ -77,6 +60,13 @@
this.closeDrawer_();
};
ns.ImportController.prototype.onBrowseBackupsClick_ = function (evt) {
$.publish(Events.DIALOG_SHOW, {
dialogId : 'browse-backups'
});
this.closeDrawer_();
};
ns.ImportController.prototype.openPiskelFile_ = function (file) {
if (this.isPiskel_(file)) {
$.publish(Events.DIALOG_SHOW, {

View File

@@ -34,7 +34,7 @@
this.saveDesktopAsNewButton = document.querySelector('#save-desktop-as-new-button');
this.saveFileDownloadButton = document.querySelector('#save-file-download-button');
this.safeAddEventListener_(this.saveLocalStorageButton, 'click', this.saveToLocalStorage_);
this.safeAddEventListener_(this.saveLocalStorageButton, 'click', this.saveToIndexedDb_);
this.safeAddEventListener_(this.saveGalleryButton, 'click', this.saveToGallery_);
this.safeAddEventListener_(this.saveDesktopButton, 'click', this.saveToDesktop_);
this.safeAddEventListener_(this.saveDesktopAsNewButton, 'click', this.saveToDesktopAsNew_);
@@ -99,7 +99,7 @@
if (pskl.app.isLoggedIn()) {
this.saveToGallery_();
} else {
this.saveToLocalStorage_();
this.saveToIndexedDb_();
}
};
@@ -111,8 +111,8 @@
this.saveTo_('saveToGallery', false);
};
ns.SaveController.prototype.saveToLocalStorage_ = function () {
this.saveTo_('saveToLocalStorage', false);
ns.SaveController.prototype.saveToIndexedDb_ = function () {
this.saveTo_('saveToIndexedDb', false);
};
ns.SaveController.prototype.saveToDesktop_ = function () {

View File

@@ -14,7 +14,6 @@
pskl.utils.inherit(ns.GifExportController, pskl.controller.settings.AbstractSettingController);
ns.GifExportController.prototype.init = function () {
this.uploadStatusContainerEl = document.querySelector('.gif-upload-status');
this.previewContainerEl = document.querySelector('.gif-export-preview');
this.uploadButton = document.querySelector('.gif-upload-button');
@@ -22,6 +21,10 @@
this.addEventListener(this.uploadButton, 'click', this.onUploadButtonClick_);
this.addEventListener(this.downloadButton, 'click', this.onDownloadButtonClick_);
var currentColors = pskl.app.currentColorsService.getCurrentColors();
var tooManyColors = currentColors.length >= MAX_GIF_COLORS;
document.querySelector('.gif-export-warning').classList.toggle('visible', tooManyColors);
};
ns.GifExportController.prototype.getZoom_ = function () {

View File

@@ -207,6 +207,14 @@
};
ns.PngExportController.prototype.onDataUriClick_ = function (evt) {
window.open(this.createPngSpritesheet_().toDataURL('image/png'));
var popup = window.open('about:blank');
var dataUri = this.createPngSpritesheet_().toDataURL('image/png');
window.setTimeout(function () {
var html = pskl.utils.Template.getAndReplace('data-uri-export-partial', {
src: dataUri
});
popup.document.title = dataUri;
popup.document.body.innerHTML = html;
}.bind(this), 500);
};
})();

View File

@@ -0,0 +1,258 @@
(function () {
var ns = $.namespace('pskl.database');
var DB_NAME = 'PiskelSessionsDatabase';
var DB_VERSION = 1;
// Simple wrapper to promisify a request.
var _requestPromise = function (req) {
var deferred = Q.defer();
req.onsuccess = deferred.resolve.bind(deferred);
req.onerror = deferred.reject.bind(deferred);
return deferred.promise;
};
/**
* The BackupDatabase handles all the database interactions related
* to piskel snapshots continuously saved while during the usage of
* Piskel.
*/
ns.BackupDatabase = function () {
this.db = null;
};
ns.BackupDatabase.DB_NAME = DB_NAME;
/**
* Open and initialize the database.
* Returns a promise that resolves when the databse is opened.
*/
ns.BackupDatabase.prototype.init = function () {
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = this.onUpgradeNeeded_.bind(this);
return _requestPromise(request).then(function (event) {
this.db = event.target.result;
return this.db;
}.bind(this)).catch(function (e) {
console.log('Could not initialize the piskel backup database');
});
};
ns.BackupDatabase.prototype.onUpgradeNeeded_ = function (event) {
// Set this.db early to allow migration scripts to access it in oncomplete.
this.db = event.target.result;
// Create an object store "piskels" with the autoIncrement flag set as true.
var objectStore = this.db.createObjectStore('snapshots', { keyPath: 'id', autoIncrement : true });
objectStore.createIndex('session_id', 'session_id', { unique: false });
objectStore.createIndex('date', 'date', { unique: false });
objectStore.createIndex('session_id, date', ['session_id', 'date'], { unique: false });
objectStore.transaction.oncomplete = function(event) {
// Nothing to do at the moment!
}.bind(this);
};
ns.BackupDatabase.prototype.openObjectStore_ = function () {
return this.db.transaction(['snapshots'], 'readwrite').objectStore('snapshots');
};
/**
* Send an add request for the provided snapshot.
* Returns a promise that resolves the request event.
*/
ns.BackupDatabase.prototype.createSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.add(snapshot);
return _requestPromise(request);
};
/**
* Send a put request for the provided snapshot.
* Returns a promise that resolves the request event.
*/
ns.BackupDatabase.prototype.updateSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.put(snapshot);
return _requestPromise(request);
};
/**
* Send a delete request for the provided snapshot.
* Returns a promise that resolves the request event.
*/
ns.BackupDatabase.prototype.deleteSnapshot = function (snapshot) {
var objectStore = this.openObjectStore_();
var request = objectStore.delete(snapshot.id);
return _requestPromise(request);
};
/**
* Send a get request for the provided snapshotId.
* Returns a promise that resolves the request event.
*/
ns.BackupDatabase.prototype.getSnapshot = function (snapshotId) {
var objectStore = this.openObjectStore_();
var request = objectStore.get(snapshotId);
return _requestPromise(request).then(function (event) {
return event.target.result;
});
};
/**
* Get the last (most recent) snapshot that satisfies the accept filter provided.
* Returns a promise that will resolve with the first matching snapshot (or null
* if no valid snapshot is found).
*
* @param {Function} accept:
* Filter method that takes a snapshot as argument and should return true
* if the snapshot is valid.
*/
ns.BackupDatabase.prototype.findLastSnapshot = function (accept) {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
var index = objectStore.index('date');
var range = IDBKeyRange.upperBound(Infinity);
index.openCursor(range, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
var snapshot = cursor && cursor.value;
// Resolve null if we couldn't find a matching snapshot.
if (!snapshot) {
deferred.resolve(null);
} else if (accept(snapshot)) {
deferred.resolve(snapshot);
} else {
cursor.continue();
}
};
return deferred.promise;
};
/**
* Retrieve all the snapshots for a given session id, sorted by descending date order.
* Returns a promise that resolves with an array of snapshots.
*
* @param {String} sessionId
* The session id
*/
ns.BackupDatabase.prototype.getSnapshotsBySessionId = function (sessionId) {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
// Loop on all the saved snapshots for the provided piskel id
var index = objectStore.index('session_id, date');
var keyRange = IDBKeyRange.bound(
[sessionId, 0],
[sessionId, Infinity]
);
var snapshots = [];
// Ordered by date in descending order.
index.openCursor(keyRange, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
snapshots.push(cursor.value);
cursor.continue();
} else {
// Consumed all piskel snapshots
deferred.resolve(snapshots);
}
};
return deferred.promise;
};
ns.BackupDatabase.prototype.getSessions = function () {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.db.transaction(['snapshots']).objectStore('snapshots');
var sessions = {};
var _createSession = function (snapshot) {
sessions[snapshot.session_id] = {
startDate: snapshot.date,
endDate: snapshot.date,
name: snapshot.name,
description: snapshot.description,
id: snapshot.session_id,
count: 1
};
};
var _updateSession = function (snapshot) {
var s = sessions[snapshot.session_id];
s.startDate = Math.min(s.startDate, snapshot.date);
s.endDate = Math.max(s.endDate, snapshot.date);
s.count++;
if (s.endDate === snapshot.date) {
// If the endDate was updated, update also the session metadata to
// reflect the latest state.
s.name = snapshot.name;
s.description = snapshot.description;
}
};
var index = objectStore.index('date');
var range = IDBKeyRange.upperBound(Infinity);
index.openCursor(range, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
var snapshot = cursor && cursor.value;
if (!snapshot) {
deferred.resolve(sessions);
} else {
if (sessions[snapshot.session_id]) {
_updateSession(snapshot);
} else {
_createSession(snapshot);
}
cursor.continue();
}
};
return deferred.promise.then(function (sessions) {
// Convert the sessions map to an array.
return Object.keys(sessions).map(function (key) {
return sessions[key];
});
});
};
ns.BackupDatabase.prototype.deleteSnapshotsForSession = function (sessionId) {
// Create the backup promise.
var deferred = Q.defer();
// Open a transaction to the snapshots object store.
var objectStore = this.openObjectStore_();
// Loop on all the saved snapshots for the provided piskel id
var index = objectStore.index('session_id');
var keyRange = IDBKeyRange.only(sessionId);
index.openCursor(keyRange).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
cursor.delete();
cursor.continue();
} else {
deferred.resolve();
}
};
return deferred.promise;
};
})();

View File

@@ -0,0 +1,141 @@
(function () {
var ns = $.namespace('pskl.database');
var DB_NAME = 'PiskelDatabase';
var DB_VERSION = 1;
// Simple wrapper to promisify a request.
var _requestPromise = function (req) {
var deferred = Q.defer();
req.onsuccess = deferred.resolve.bind(deferred);
req.onerror = deferred.reject.bind(deferred);
return deferred.promise;
};
/**
* The PiskelDatabase handles all the database interactions related
* to the local piskel saved that can be performed in-browser.
*/
ns.PiskelDatabase = function (options) {
this.db = null;
};
ns.PiskelDatabase.DB_NAME = DB_NAME;
ns.PiskelDatabase.prototype.init = function () {
var request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = this.onUpgradeNeeded_.bind(this);
return _requestPromise(request).then(function (event) {
this.db = event.target.result;
return this.db;
}.bind(this)).catch(function (e) {
console.log('Failed to initialize IndexedDB, local browser saves will be unavailable.');
});
};
ns.PiskelDatabase.prototype.onUpgradeNeeded_ = function (event) {
// Set this.db early to allow migration scripts to access it in oncomplete.
this.db = event.target.result;
// Create an object store "piskels" with the autoIncrement flag set as true.
var objectStore = this.db.createObjectStore('piskels', { keyPath : 'name' });
objectStore.transaction.oncomplete = function(event) {
pskl.database.migrate.MigrateLocalStorageToIndexedDb.migrate(this);
}.bind(this);
};
ns.PiskelDatabase.prototype.openObjectStore_ = function () {
return this.db.transaction(['piskels'], 'readwrite').objectStore('piskels');
};
/**
* Send a get request for the provided name.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.get = function (name) {
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.get(name)).then(function (event) {
return event.target.result;
});
};
/**
* List all locally saved piskels.
* Returns a promise that resolves an array of objects:
* - name: name of the piskel
* - description: description of the piskel
* - date: save date
*
* The sprite content is not contained in the object and
* needs to be retrieved with a separate get.
*/
ns.PiskelDatabase.prototype.list = function () {
var deferred = Q.defer();
var piskels = [];
var objectStore = this.openObjectStore_();
var cursor = objectStore.openCursor();
cursor.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
piskels.push({
name: cursor.value.name,
date: cursor.value.date,
description: cursor.value.description
});
cursor.continue();
} else {
// Cursor consumed all availabled piskels
deferred.resolve(piskels);
}
};
cursor.onerror = function () {
deferred.reject();
};
return deferred.promise;
};
/**
* Send an put request for the provided args.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.update = function (name, description, date, serialized) {
var data = {};
data.name = name;
data.serialized = serialized;
data.date = date;
data.description = description;
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.put(data));
};
/**
* Send an add request for the provided args.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.create = function (name, description, date, serialized) {
var data = {};
data.name = name;
data.serialized = serialized;
data.date = date;
data.description = description;
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.add(data));
};
/**
* Delete a saved piskel for the provided name.
* Returns a promise that resolves the request event.
*/
ns.PiskelDatabase.prototype.delete = function (name) {
var objectStore = this.openObjectStore_();
return _requestPromise(objectStore.delete(name));
};
})();

View File

@@ -0,0 +1,76 @@
(function () {
var ns = $.namespace('pskl.database.migrate');
// Simple migration helper to move local storage saves to indexed db.
ns.MigrateLocalStorageToIndexedDb = {};
ns.MigrateLocalStorageToIndexedDb.migrate = function (piskelDatabase) {
var deferred = Q.defer();
var localStorageService = pskl.app.localStorageService;
var localStorageKeys = localStorageService.getKeys();
var migrationData = localStorageKeys.map(function (key) {
return {
name: key.name,
description: key.description,
date: key.date,
serialized: localStorageService.getPiskel(key.name)
};
});
// Define the sequential migration process.
// Wait for each sprite to be saved before saving the next one.
var success = true;
var migrateSprite = function (index) {
var data = migrationData[index];
if (!data) {
console.log('Data migration from local storage to indexed db finished.');
if (success) {
console.log('Local storage piskels successfully migrated. Old copies will be deleted.');
ns.MigrateLocalStorageToIndexedDb.deleteLocalStoragePiskels();
}
deferred.resolve();
} else {
ns.MigrateLocalStorageToIndexedDb.save_(piskelDatabase, data)
.then(function () {
migrateSprite(index + 1);
})
.catch(function (e) {
var success = false;
console.error('Failed to migrate local storage sprite for name: ' + data.name);
migrateSprite(index + 1);
});
}
};
// Start the migration.
migrateSprite(0);
return deferred.promise;
};
ns.MigrateLocalStorageToIndexedDb.save_ = function (piskelDatabase, piskelData) {
return piskelDatabase.get(piskelData.name).then(function (data) {
if (typeof data !== 'undefined') {
return piskelDatabase.update(piskelData.name, piskelData.description, piskelData.date, piskelData.serialized);
} else {
return piskelDatabase.create(piskelData.name, piskelData.description, piskelData.date, piskelData.serialized);
}
});
};
ns.MigrateLocalStorageToIndexedDb.deleteLocalStoragePiskels = function () {
var localStorageKeys = pskl.app.localStorageService.getKeys();
// Remove all sprites.
localStorageKeys.forEach(function (key) {
window.localStorage.removeItem('piskel.' + key.name);
});
// Remove keys.
window.localStorage.removeItem('piskel.keys');
};
})();

View File

@@ -108,6 +108,8 @@
this.playTransformToolEvent_(recordEvent);
} else if (recordEvent.type === 'instrumented-event') {
this.playInstrumentedEvent_(recordEvent);
} else if (recordEvent.type === 'clipboard-event') {
this.playClipboardEvent_(recordEvent);
}
// Record the time spent replaying the event
@@ -169,6 +171,16 @@
pskl.app.piskelController[recordEvent.methodName].apply(pskl.app.piskelController, recordEvent.args);
};
ns.DrawingTestPlayer.prototype.playClipboardEvent_ = function (recordEvent) {
$.publish(recordEvent.event.type, {
preventDefault: function () {},
clipboardData: {
items: [],
setData: function () {}
}
});
};
ns.DrawingTestPlayer.prototype.onTestEnd_ = function () {
this.removeMouseShim_();
// Restore the original drawing loop.

View File

@@ -15,6 +15,10 @@
$.subscribe(Events.TRANSFORMATION_EVENT, this.onTransformationEvent_.bind(this));
$.subscribe(Events.PRIMARY_COLOR_SELECTED, this.onColorEvent_.bind(this, true));
$.subscribe(Events.SECONDARY_COLOR_SELECTED, this.onColorEvent_.bind(this, false));
$.subscribe(Events.CLIPBOARD_COPY, this.onClipboardEvent_.bind(this));
$.subscribe(Events.CLIPBOARD_CUT, this.onClipboardEvent_.bind(this));
$.subscribe(Events.CLIPBOARD_PASTE, this.onClipboardEvent_.bind(this));
for (var key in this.piskelController) {
if (typeof this.piskelController[key] == 'function') {
@@ -136,6 +140,15 @@
}
};
ns.DrawingTestRecorder.prototype.onClipboardEvent_ = function (evt) {
if (this.isRecording) {
var recordEvent = {};
recordEvent.type = 'clipboard-event';
recordEvent.event = evt;
this.events.push(recordEvent);
}
};
ns.DrawingTestRecorder.prototype.onInstrumentedMethod_ = function (callee, methodName, args) {
if (this.isRecording) {
var recordEvent = {};

View File

@@ -16,7 +16,6 @@
this.descriptor = descriptor;
this.savePath = null;
this.fps = fps;
} else {
throw 'Missing arguments in Piskel constructor : ' + Array.prototype.join.call(arguments, ',');
}

View File

@@ -5,9 +5,23 @@
this.reset();
};
ns.BaseSelection.prototype.stringify = function () {
return JSON.stringify({
pixels: this.pixels,
time: this.time
});
};
ns.BaseSelection.prototype.parse = function (str) {
var selectionData = JSON.parse(str);
this.pixels = selectionData.pixels;
this.time = selectionData.time;
};
ns.BaseSelection.prototype.reset = function () {
this.pixels = [];
this.hasPastedContent = false;
this.time = -1;
};
ns.BaseSelection.prototype.move = function (colDiff, rowDiff) {
@@ -30,5 +44,8 @@
});
this.hasPastedContent = true;
// Keep track of the selection time to compare between local selection and
// paste event selections.
this.time = Date.now();
};
})();

View File

@@ -17,14 +17,11 @@
$.subscribe(Events.SELECTION_CREATED, $.proxy(this.onSelectionCreated_, this));
$.subscribe(Events.SELECTION_DISMISSED, $.proxy(this.onSelectionDismissed_, this));
$.subscribe(Events.SELECTION_MOVE_REQUEST, $.proxy(this.onSelectionMoved_, this));
$.subscribe(Events.SELECTION_COPY, this.copy.bind(this));
$.subscribe(Events.SELECTION_CUT, this.cut.bind(this));
$.subscribe(Events.SELECTION_PASTE, this.paste.bind(this));
$.subscribe(Events.CLIPBOARD_COPY, this.copy.bind(this));
$.subscribe(Events.CLIPBOARD_CUT, this.copy.bind(this));
$.subscribe(Events.CLIPBOARD_PASTE, this.paste.bind(this));
var shortcuts = pskl.service.keyboard.Shortcuts;
pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.PASTE, this.paste.bind(this));
pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.CUT, this.cut.bind(this));
pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.COPY, this.copy.bind(this));
pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.DELETE, this.onDeleteShortcut_.bind(this));
pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.COMMIT, this.commit.bind(this));
@@ -78,29 +75,85 @@
scope : this,
replay : {
type : SELECTION_REPLAY.ERASE,
pixels : JSON.parse(JSON.stringify(pixels.slice(0)))
pixels : JSON.parse(JSON.stringify(pixels))
}
});
};
ns.SelectionManager.prototype.cut = function() {
if (this.currentSelection) {
// Put cut target into the selection:
ns.SelectionManager.prototype.copy = function(event, domEvent) {
if (this.currentSelection && this.piskelController.getCurrentFrame()) {
this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame());
this.erase();
if (domEvent) {
domEvent.clipboardData.setData('text/plain', this.currentSelection.stringify());
domEvent.preventDefault();
}
if (event.type === Events.CLIPBOARD_CUT) {
this.erase();
}
}
};
ns.SelectionManager.prototype.paste = function() {
if (!this.currentSelection || !this.currentSelection.hasPastedContent) {
if (window.localStorage.getItem('piskel.clipboard')) {
this.currentSelection = JSON.parse(window.localStorage.getItem('piskel.clipboard'));
} else {
return;
ns.SelectionManager.prototype.paste = function(event, domEvent) {
var items = domEvent ? domEvent.clipboardData.items : [];
try {
for (var i = 0 ; i < items.length ; i++) {
var item = items[i];
if (/^image/i.test(item.type)) {
this.pasteImage_(item);
event.stopPropagation();
return;
}
if (/^text\/plain/i.test(item.type)) {
this.pasteText_(item);
event.stopPropagation();
return;
}
}
} catch (e) {
// Some of the clipboard APIs are not available on Safari/IE
// Allow Piskel to fallback on local currentSelection pasting.
}
var pixels = this.currentSelection.pixels;
// temporarily keeping this code path for tests and fallbacks.
if (this.currentSelection && this.currentSelection.hasPastedContent) {
this.pastePixelsOnCurrentFrame_(this.currentSelection.pixels);
}
};
ns.SelectionManager.prototype.pasteImage_ = function(clipboardItem) {
var blob = clipboardItem.getAsFile();
pskl.utils.FileUtils.readImageFile(blob, function (image) {
pskl.app.fileDropperService.dropPosition_ = {x: 0, y: 0};
pskl.app.fileDropperService.onImageLoaded_(image, blob);
}.bind(this));
};
ns.SelectionManager.prototype.pasteText_ = function(clipboardItem) {
var blob = clipboardItem.getAsString(function (selectionString) {
var selectionData = JSON.parse(selectionString);
var time = selectionData.time;
var pixels = selectionData.pixels;
if (this.currentSelection && this.currentSelection.time >= time) {
// If the local selection is newer or equal to the one coming from the clipboard event
// use the local one. The reason is that the "move" information is only updated locally
// without synchronizing it to the clipboard.
// TODO: the selection should store the origin of the selection and the selection itself
// separately.
pixels = this.currentSelection.pixels;
}
if (pixels) {
// If the current clipboard data is some random text, pixels will not be defined.
this.pastePixelsOnCurrentFrame_(pixels);
}
}.bind(this));
};
ns.SelectionManager.prototype.pastePixelsOnCurrentFrame_ = function (pixels) {
var frame = this.piskelController.getCurrentFrame();
this.pastePixels_(frame, pixels);
@@ -123,8 +176,7 @@
var tool = pskl.app.drawingController.currentToolBehavior;
var isSelectionTool = tool instanceof pskl.tools.drawing.selection.BaseSelect;
if (isSelectionTool) {
var overlay = pskl.app.drawingController.overlayFrame;
tool.commitSelection(overlay);
tool.commitSelection();
}
};
@@ -147,13 +199,6 @@
});
};
ns.SelectionManager.prototype.copy = function() {
if (this.currentSelection && this.piskelController.getCurrentFrame()) {
this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame());
window.localStorage.setItem('piskel.clipboard', JSON.stringify(this.currentSelection));
}
};
/**
* @private
*/

View File

@@ -1,65 +1,161 @@
(function () {
var ns = $.namespace('pskl.service');
// 1 minute = 1000 * 60
var BACKUP_INTERVAL = 1000 * 60;
var ONE_SECOND = 1000;
var ONE_MINUTE = 60 * ONE_SECOND;
ns.BackupService = function (piskelController) {
// Save every minute = 1000 * 60
var BACKUP_INTERVAL = ONE_MINUTE;
// Store a new snapshot every 5 minutes.
var SNAPSHOT_INTERVAL = ONE_MINUTE * 5;
// Store up to 12 snapshots for a piskel session, min. 1 hour of work
var MAX_SNAPSHOTS_PER_SESSION = 12;
var MAX_SESSIONS = 10;
ns.BackupService = function (piskelController, backupDatabase) {
this.piskelController = piskelController;
this.lastHash = null;
// Immediately store the current when initializing the Service to avoid storing
// empty sessions.
this.lastHash = this.piskelController.getPiskel().getHash();
this.nextSnapshotDate = -1;
// backupDatabase can be provided for testing purposes.
this.backupDatabase = backupDatabase || new pskl.database.BackupDatabase();
};
ns.BackupService.prototype.init = function () {
var previousPiskel = window.localStorage.getItem('bkp.next.piskel');
var previousInfo = window.localStorage.getItem('bkp.next.info');
if (previousPiskel && previousInfo) {
this.savePiskel_('prev', previousPiskel, previousInfo);
}
this.backupDatabase.init().then(function () {
window.setInterval(this.backup.bind(this), BACKUP_INTERVAL);
}.bind(this));
};
window.setInterval(this.backup.bind(this), BACKUP_INTERVAL);
// This is purely exposed for testing, so that backup dates can be set programmatically.
ns.BackupService.prototype.currentDate_ = function () {
return Date.now();
};
ns.BackupService.prototype.backup = function () {
var piskel = this.piskelController.getPiskel();
var descriptor = piskel.getDescriptor();
var hash = piskel.getHash();
var info = {
name : descriptor.name,
description : descriptor.info,
date : Date.now(),
hash : hash
};
// Do not save an unchanged piskel
if (hash !== this.lastHash) {
this.lastHash = hash;
var serializedPiskel = pskl.utils.serialization.Serializer.serialize(piskel);
this.savePiskel_('next', serializedPiskel, JSON.stringify(info));
if (hash === this.lastHash) {
return Q.resolve();
}
};
ns.BackupService.prototype.getPreviousPiskelInfo = function () {
var previousInfo = window.localStorage.getItem('bkp.prev.info');
if (previousInfo) {
return JSON.parse(previousInfo);
}
};
// Update the hash
// TODO: should only be done after a successful save.
this.lastHash = hash;
ns.BackupService.prototype.load = function() {
var previousPiskel = window.localStorage.getItem('bkp.prev.piskel');
previousPiskel = JSON.parse(previousPiskel);
// Prepare the backup snapshot.
var descriptor = piskel.getDescriptor();
var date = this.currentDate_();
var snapshot = {
session_id: pskl.app.sessionId,
date: date,
name: descriptor.name,
description: descriptor.description,
frames: piskel.getFrameCount(),
width: piskel.getWidth(),
height: piskel.getHeight(),
fps: piskel.getFPS(),
serialized: pskl.utils.serialization.Serializer.serialize(piskel)
};
pskl.utils.serialization.Deserializer.deserialize(previousPiskel, function (piskel) {
pskl.app.piskelController.setPiskel(piskel);
return this.getSnapshotsBySessionId(pskl.app.sessionId).then(function (snapshots) {
var latest = snapshots[0];
if (latest && date < this.nextSnapshotDate) {
// update the latest snapshot
snapshot.id = latest.id;
return this.backupDatabase.updateSnapshot(snapshot);
} else {
// add a new snapshot
this.nextSnapshotDate = date + SNAPSHOT_INTERVAL;
return this.backupDatabase.createSnapshot(snapshot).then(function () {
if (snapshots.length >= MAX_SNAPSHOTS_PER_SESSION) {
// remove oldest snapshot
return this.backupDatabase.deleteSnapshot(snapshots[snapshots.length - 1]);
}
}.bind(this)).then(function () {
var isNewSession = !latest;
if (!isNewSession) {
return;
}
return this.backupDatabase.getSessions().then(function (sessions) {
if (sessions.length <= MAX_SESSIONS) {
// If MAX_SESSIONS has not been reached, no need to delete
// previous sessions.
return;
}
// Prepare an array containing all the ids of the sessions to be deleted.
var sessionIdsToDelete = sessions.sort(function (s1, s2) {
return s1.startDate - s2.startDate;
}).map(function (s) {
return s.id;
}).slice(0, sessions.length - MAX_SESSIONS);
// Delete all the extra sessions.
return Q.all(sessionIdsToDelete.map(function (id) {
return this.deleteSession(id);
}.bind(this)));
}.bind(this));
}.bind(this));
}
}.bind(this)).catch(function (e) {
console.error(e);
});
};
ns.BackupService.prototype.savePiskel_ = function (type, piskel, info) {
try {
window.localStorage.setItem('bkp.' + type + '.piskel', piskel);
window.localStorage.setItem('bkp.' + type + '.info', info);
} catch (e) {
console.error('Could not save piskel backup in localStorage.', e);
}
ns.BackupService.prototype.getSnapshotsBySessionId = function (sessionId) {
return this.backupDatabase.getSnapshotsBySessionId(sessionId);
};
ns.BackupService.prototype.deleteSession = function (sessionId) {
return this.backupDatabase.deleteSnapshotsForSession(sessionId);
};
ns.BackupService.prototype.getPreviousPiskelInfo = function () {
return this.backupDatabase.findLastSnapshot(function (snapshot) {
return snapshot.session_id !== pskl.app.sessionId;
});
};
ns.BackupService.prototype.list = function() {
return this.backupDatabase.getSessions();
};
ns.BackupService.prototype.loadSnapshotById = function(snapshotId) {
var deferred = Q.defer();
this.backupDatabase.getSnapshot(snapshotId).then(function (snapshot) {
pskl.utils.serialization.Deserializer.deserialize(
JSON.parse(snapshot.serialized),
function (piskel) {
pskl.app.piskelController.setPiskel(piskel);
deferred.resolve();
}
);
});
return deferred.promise;
};
// Load "latest" backup snapshot.
ns.BackupService.prototype.load = function() {
var deferred = Q.defer();
this.getPreviousPiskelInfo().then(function (snapshot) {
pskl.utils.serialization.Deserializer.deserialize(
JSON.parse(snapshot.serialized),
function (piskel) {
pskl.app.piskelController.setPiskel(piskel);
deferred.resolve();
}
);
});
return deferred.promise;
};
})();

View File

@@ -30,6 +30,8 @@
};
ns.BeforeUnloadService.prototype.onBeforeUnload = function (evt) {
// Attempt one last backup. Some of it may fail due to the asynchronous
// nature of IndexedDB.
pskl.app.backupService.backup();
if (pskl.app.savedStatusService.isDirty()) {
var confirmationMessage = 'Your current sprite has unsaved changes. Are you sure you want to quit?';

View File

@@ -0,0 +1,25 @@
(function () {
var ns = $.namespace('pskl.service');
ns.ClipboardService = function (piskelController) {
this.piskelController = piskelController;
};
ns.ClipboardService.prototype.init = function () {
window.addEventListener('copy', this._onCopy.bind(this), true);
window.addEventListener('cut', this._onCut.bind(this), true);
window.addEventListener('paste', this._onPaste.bind(this), true);
};
ns.ClipboardService.prototype._onCut = function (event) {
$.publish(Events.CLIPBOARD_CUT, event);
};
ns.ClipboardService.prototype._onCopy = function (event) {
$.publish(Events.CLIPBOARD_COPY, event);
};
ns.ClipboardService.prototype._onPaste = function (event) {
$.publish(Events.CLIPBOARD_PASTE, event);
};
})();

View File

@@ -35,13 +35,9 @@
var isPiskel = /\.piskel$/i.test(file.name);
var isPalette = /\.(gpl|txt|pal)$/i.test(file.name);
if (isImage) {
$.publish(Events.DIALOG_SHOW, {
dialogId : 'import',
initArgs : {
rawFiles: [file]
}
});
// pskl.utils.FileUtils.readImageFile(file, this.onImageLoaded_.bind(this));
pskl.utils.FileUtils.readImageFile(file, function (image) {
this.onImageLoaded_(image, file);
}.bind(this));
} else if (isPiskel) {
pskl.utils.PiskelFileUtils.loadFromFile(file, this.onPiskelFileLoaded_, this.onPiskelFileError_);
} else if (isPalette) {
@@ -56,7 +52,7 @@
};
ns.FileDropperService.prototype.onPiskelFileLoaded_ = function (piskel) {
if (window.confirm('This will replace your current animation')) {
if (window.confirm(Constants.CONFIRM_OVERWRITE)) {
pskl.app.piskelController.setPiskel(piskel);
}
};
@@ -65,10 +61,23 @@
$.publish(Events.PISKEL_FILE_IMPORT_FAILED, [reason]);
};
ns.FileDropperService.prototype.onImageLoaded_ = function (importedImage) {
ns.FileDropperService.prototype.onImageLoaded_ = function (importedImage, file) {
var piskelWidth = pskl.app.piskelController.getWidth();
var piskelHeight = pskl.app.piskelController.getHeight();
if (this.isMultipleFiles_) {
this.piskelController.addFrameAtCurrentIndex();
this.piskelController.selectNextFrame();
} else if (importedImage.width > piskelWidth || importedImage.height > piskelHeight) {
// For single file imports, if the file is too big, trigger the import wizard.
$.publish(Events.DIALOG_SHOW, {
dialogId : 'import',
initArgs : {
rawFiles: [file]
}
});
return;
}
var currentFrame = this.piskelController.getCurrentFrame();

View File

@@ -15,10 +15,17 @@
data : imageData
};
var protocol = pskl.utils.Environment.isHttps() ? 'https' : 'http';
var wrappedSuccess = function (response) {
success(Constants.IMAGE_SERVICE_GET_URL + response.responseText);
var getUrl = pskl.utils.Template.replace(Constants.IMAGE_SERVICE_GET_URL, {
protocol: protocol
});
success(getUrl + response.responseText);
};
pskl.utils.Xhr.post(Constants.IMAGE_SERVICE_UPLOAD_URL, data, wrappedSuccess, error);
var uploadUrl = pskl.utils.Template.replace(Constants.IMAGE_SERVICE_UPLOAD_URL, {
protocol: protocol
});
pskl.utils.Xhr.post(uploadUrl, data, wrappedSuccess, error);
};
})();

View File

@@ -0,0 +1,48 @@
(function () {
var ns = $.namespace('pskl.service.keyboard');
ns.InputService = function () {
this.activeShortcuts_ = {};
};
/**
* @public
*/
ns.InputService.prototype.init = function() {
$(document.body).keydown($.proxy(this.onKeyDown_, this));
$(document.body).keyup($.proxy(this.onKeyUp_, this));
};
ns.InputService.prototype.isKeyPressed = function (shortcut) {
return shortcut.getKeys().some(function (key) {
return this.activeShortcuts_[key];
}.bind(this));
};
/**
* @private
*/
ns.InputService.prototype.onKeyDown_ = function(evt) {
var eventKey = ns.KeyUtils.createKeyFromEvent(evt);
if (this.isInInput_(evt) || !eventKey) {
return;
}
this.activeShortcuts_[ns.KeyUtils.stringify(eventKey)] = true;
};
ns.InputService.prototype.onKeyUp_ = function(evt) {
var eventKey = ns.KeyUtils.createKeyFromEvent(evt);
if (this.isInInput_(evt) || !eventKey) {
return;
}
this.activeShortcuts_[ns.KeyUtils.stringify(eventKey)] = false;
};
ns.InputService.prototype.isInInput_ = function (evt) {
var targetTagName = evt.target.nodeName.toUpperCase();
return targetTagName === 'INPUT' || targetTagName === 'TEXTAREA';
};
})();

View File

@@ -66,6 +66,11 @@
OFFSET_RIGHT : createShortcut('move-right', 'Move viewport right', 'shift+right'),
OFFSET_DOWN : createShortcut('move-down', 'Move viewport down', 'shift+down'),
OFFSET_LEFT : createShortcut('move-left', 'Move viewport left', 'shift+left'),
CURSOR_UP : createShortcut('cursor-up', 'Move cursor up', 'alt+up'),
CURSOR_RIGHT : createShortcut('cursor-right', 'Move cursor right', 'alt+right'),
CURSOR_DOWN : createShortcut('cursor-down', 'Move cursor down', 'alt+down'),
CURSOR_LEFT : createShortcut('cursor-left', 'Move cursor left', 'alt+left'),
CURSOR_CLICK : createShortcut('cursor-click', 'Click cursor', 'SPACE'),
},
STORAGE : {
@@ -84,6 +89,10 @@
'123456789'.split(''), '1 to 9')
},
DEBUG : {
RELOAD_STYLES : createShortcut('move-left', 'Move viewport left', 'ctrl+alt+R'),
},
CATEGORIES : ['TOOL', 'SELECTION', 'MISC', 'STORAGE', 'COLOR']
};
})();

View File

@@ -3,11 +3,12 @@
ns.PaletteService = function () {
this.dynamicPalettes = [];
this.localStorageService = window.localStorage;
// Exposed for tests.
this.localStorageGlobal = window.localStorage;
};
ns.PaletteService.prototype.getPalettes = function () {
var palettesString = this.localStorageService.getItem('piskel.palettes');
var palettesString = this.localStorageGlobal.getItem('piskel.palettes');
var palettes = JSON.parse(palettesString) || [];
palettes = palettes.map(function (palette) {
return pskl.model.Palette.fromObject(palette);
@@ -54,7 +55,7 @@
palettes = palettes.filter(function (palette) {
return this.dynamicPalettes.indexOf(palette) === -1;
}.bind(this));
this.localStorageService.setItem('piskel.palettes', JSON.stringify(palettes));
this.localStorageGlobal.setItem('piskel.palettes', JSON.stringify(palettes));
$.publish(Events.PALETTE_LIST_UPDATED);
};

View File

@@ -0,0 +1,55 @@
(function () {
var ns = $.namespace('pskl.service.storage');
ns.IndexedDbStorageService = function (piskelController) {
this.piskelController = piskelController;
this.piskelDatabase = new pskl.database.PiskelDatabase();
};
ns.IndexedDbStorageService.prototype.init = function () {
this.piskelDatabase.init();
};
ns.IndexedDbStorageService.prototype.save = function (piskel) {
var name = piskel.getDescriptor().name;
var description = piskel.getDescriptor().description;
var date = Date.now();
var serialized = pskl.utils.serialization.Serializer.serialize(piskel);
return this.save_(name, description, date, serialized);
};
ns.IndexedDbStorageService.prototype.save_ = function (name, description, date, serialized) {
return this.piskelDatabase.get(name).then(function (piskelData) {
if (typeof piskelData !== 'undefined') {
return this.piskelDatabase.update(name, description, date, serialized);
} else {
return this.piskelDatabase.create(name, description, date, serialized);
}
}.bind(this));
};
ns.IndexedDbStorageService.prototype.load = function (name) {
this.piskelDatabase.get(name).then(function (piskelData) {
if (typeof piskelData !== 'undefined') {
var serialized = piskelData.serialized;
pskl.utils.serialization.Deserializer.deserialize(
JSON.parse(serialized),
function (piskel) {
pskl.app.piskelController.setPiskel(piskel);
}
);
} else {
console.log('no local browser save found for name: ' + name);
}
});
};
ns.IndexedDbStorageService.prototype.remove = function (name) {
this.piskelDatabase.delete(name);
};
ns.IndexedDbStorageService.prototype.getKeys = function () {
return this.piskelDatabase.list();
};
})();

View File

@@ -27,10 +27,15 @@
return this.delegateSave_(pskl.app.galleryStorageService, piskel);
};
// @deprecated, use saveToIndexedDb unless indexedDb is not available.
ns.StorageService.prototype.saveToLocalStorage = function (piskel) {
return this.delegateSave_(pskl.app.localStorageService, piskel);
};
ns.StorageService.prototype.saveToIndexedDb = function (piskel) {
return this.delegateSave_(pskl.app.indexedDbStorageService, piskel);
};
ns.StorageService.prototype.saveToFileDownload = function (piskel) {
return this.delegateSave_(pskl.app.fileDownloadStorageService, piskel);
};
@@ -67,7 +72,7 @@
// wrap in timeout in order to start saving only after event.preventDefault
// has been done
window.setTimeout(function () {
this.saveToLocalStorage(this.piskelController.getPiskel());
this.saveToIndexedDb(this.piskelController.getPiskel());
}.bind(this), 0);
}
};

View File

@@ -16,7 +16,7 @@
ns.AbstractDragSelect.prototype.onSelectStart_ = function (col, row, frame, overlay) {
if (this.hasSelection) {
this.hasSelection = false;
this.commitSelection(overlay);
this.commitSelection();
} else {
this.hasSelection = true;
this.onDragSelectStart_(col, row);

View File

@@ -26,6 +26,8 @@
{key : 'ctrl+v', description : 'Paste the copied area'},
{key : 'shift', description : 'Hold to move the content'}
];
$.subscribe(Events.SELECTION_DISMISSED, this.onSelectionDismissed_.bind(this));
};
pskl.utils.inherit(ns.BaseSelect, pskl.tools.drawing.BaseTool);
@@ -52,7 +54,7 @@
this.mode = 'moveSelection';
if (event.shiftKey && !this.isMovingContent_) {
this.isMovingContent_ = true;
$.publish(Events.SELECTION_CUT);
$.publish(Events.CLIPBOARD_CUT);
this.drawSelectionOnOverlay_(overlay);
}
this.onSelectionMoveStart_(col, row, frame, overlay);
@@ -111,16 +113,24 @@
};
/**
* Protected method, should be called when the selection is dismissed.
* Protected method, should be called when the selection is committed,
* typically by clicking outside of the selected area.
*/
ns.BaseSelect.prototype.commitSelection = function (overlay) {
ns.BaseSelect.prototype.commitSelection = function () {
if (this.isMovingContent_) {
$.publish(Events.SELECTION_PASTE);
$.publish(Events.CLIPBOARD_PASTE);
this.isMovingContent_ = false;
}
// Clean previous selection:
$.publish(Events.SELECTION_DISMISSED);
};
/**
* Protected method, should be called when the selection is dismissed.
*/
ns.BaseSelect.prototype.onSelectionDismissed_ = function () {
var overlay = pskl.app.drawingController.overlayFrame;
overlay.clear();
this.hasSelection = false;
};

View File

@@ -24,7 +24,7 @@
ns.ShapeSelect.prototype.onSelectStart_ = function (col, row, frame, overlay) {
if (this.hasSelection) {
this.hasSelection = false;
this.commitSelection(overlay);
this.commitSelection();
} else {
this.hasSelection = true;
// From the pixel clicked, get shape using an algorithm similar to the paintbucket one:

View File

@@ -23,7 +23,15 @@
isIntegrationTest : function () {
return window.location.href.indexOf('integration-test') !== -1;
}
},
isDebug : function () {
return window.location.href.indexOf('debug') !== -1;
},
isHttps : function () {
return window.location.href.indexOf('https://') === 0;
},
};
})();

View File

@@ -68,12 +68,21 @@
};
loadScript('piskel-script-list.js', 'loadNextScript()');
var styles;
window.loadStyles = function () {
var styles = window.pskl_exports.styles;
styles = window.pskl_exports.styles;
for (var i = 0 ; i < styles.length ; i++) {
loadStyle(styles[i]);
}
};
window.reloadStyles = function () {
for (var i = 0 ; i < styles.length ; i++) {
document.querySelector('link[href="' + styles[i] + '"]').remove();
loadStyle(styles[i]);
}
};
loadScript('piskel-style-list.js', 'loadStyles()');
} else {
var script;

View File

@@ -79,6 +79,11 @@
"js/model/Palette.js",
"js/model/Piskel.js",
// Database (IndexedDB)
"js/database/BackupDatabase.js",
"js/database/PiskelDatabase.js",
"js/database/migrate/MigrateLocalStorageToIndexedDb.js",
// Selection
"js/selection/SelectionManager.js",
"js/selection/BaseSelection.js",
@@ -144,6 +149,9 @@
"js/controller/dialogs/CreatePaletteController.js",
"js/controller/dialogs/BrowseLocalController.js",
"js/controller/dialogs/CheatsheetController.js",
"js/controller/dialogs/backups/steps/SelectSession.js",
"js/controller/dialogs/backups/steps/SessionDetails.js",
"js/controller/dialogs/backups/BrowseBackups.js",
"js/controller/dialogs/importwizard/steps/AbstractImportStep.js",
"js/controller/dialogs/importwizard/steps/AdjustSize.js",
"js/controller/dialogs/importwizard/steps/ImageImport.js",
@@ -170,6 +178,7 @@
// Services
"js/service/storage/StorageService.js",
"js/service/storage/FileDownloadStorageService.js",
"js/service/storage/IndexedDbStorageService.js",
"js/service/storage/LocalStorageService.js",
"js/service/storage/GalleryStorageService.js",
"js/service/storage/DesktopStorageService.js",
@@ -188,6 +197,7 @@
"js/service/palette/PaletteImportService.js",
"js/service/pensize/PenSizeService.js",
"js/service/SavedStatusService.js",
"js/service/keyboard/InputService.js",
"js/service/keyboard/KeycodeTranslator.js",
"js/service/keyboard/KeyUtils.js",
"js/service/keyboard/Shortcut.js",
@@ -195,15 +205,13 @@
"js/service/keyboard/ShortcutService.js",
"js/service/ImportService.js",
"js/service/ImageUploadService.js",
"js/service/ClipboardService.js",
"js/service/CurrentColorsService.js",
"js/service/FileDropperService.js",
"js/service/SelectedColorsService.js",
"js/service/MouseStateService.js",
"js/service/performance/PerformanceReport.js",
"js/service/performance/PerformanceReportService.js",
"js/service/storage/LocalStorageService.js",
"js/service/storage/GalleryStorageService.js",
"js/service/storage/DesktopStorageService.js",
// Tools
"js/tools/ToolsHelper.js",

View File

@@ -18,6 +18,7 @@
"css/icons.css",
"css/color-picker-slider.css",
"css/dialogs.css",
"css/dialogs-browse-backups.css",
"css/dialogs-browse-local.css",
"css/dialogs-cheatsheet.css",
"css/dialogs-create-palette.css",

View File

@@ -0,0 +1,70 @@
<div style="display:none">
<script type="text/template" id="data-uri-export-partial">
<style>
html, body {
margin: 0;
}
body {
background: #444;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: monospace;
}
#image-wrapper {
flex-grow: 1;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
img {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABlBMVEXf39////8zI3BgAAAAHklEQVR4AWNghAIGCMDgjwgFCDDSw2M0PSCD0fQAACRcAgF4ciGUAAAAAElFTkSuQmCC') repeat;
}
#bottom-wrapper {
width: 600px;
height: 300px;
margin-top: 20px;
position: relative;
flex-shrink: 0;
}
textarea {
position: absolute;
bottom: 0;
width: 100%;
height: 100%;
padding: 10px;
border-style: none;
background: black;
color: gold;
font-family: monospace;
}
span {
position: absolute;
bottom: 10px;
left: 10px;
font-size: 12px;
padding: 5px;
color: white;
background-color: #444;
}
</style>
<body>
<div id="image-wrapper">
<img src="{{src}}" alt="Exported image as data-uri">
</div>
<div id="bottom-wrapper">
<textarea spellcheck="false" onclick="this.select()">{{src}}</textarea>
<span>Data-uri for the exported framesheet</span>
</div>
</body>
</script>
</div>

View File

@@ -0,0 +1,75 @@
<script type="text/template" id="templates/dialogs/browse-backups.html">
<div class="dialog-wrapper">
<h3 class="dialog-head">
Browse backups
<span class="dialog-close">X</span>
</h3>
<div class="dialog-content backups-wizard-container"></div>
</div>
</script>
<script type="text/template" id="backups-select-session">
<div class="backups-step-container">
<div class="backups-step-content">
<div class="browse-backups-disclaimer">
<div class="backups-icon icon-common-backup-white">&nbsp;</div>
<div class="browse-backups-disclaimer-content">
<!-- Keep in sync with MAX_SESSIONS in BackupService.js -->
If you forgot to save your work or if Piskel crashed, try to restore one of the automatically backed up sessions below (up to 10 sessions).
<br/>
<br/>
Backups may be erased without notice, so try to save your work to a file or to your gallery as soon as you can.
</div>
</div>
<div class="session-list">
</div>
</div>
</div>
</script>
<script type="text/template" id="session-list-empty">
<div class="session-list-empty">No session found ...</div>
</script>
<script type="text/template" id="session-list-item">
<div class="session-item">
<div class="session-details">
<span class="session-details-title">{{name}} {{description}}</span>
<span class="session-details-info">Session recorded {{date}}</span>
<span class="session-details-info">{{count}} saved</span>
</div>
<div class="session-actions">
<button class="button" data-session-id="{{id}}" data-action="delete">Delete</button>
<button class="button button-primary" data-session-id="{{id}}" data-action="view">View</button>
</div>
</div>
</script>
<script type="text/template" id="backups-session-details">
<div class="backups-step-container">
<div class="backups-step-content">
<div class="snapshot-list"></div>
</div>
<div class="backups-step-actions">
<button class="button back-button">back</button>
</div>
</div>
</script>
<script type="text/template" id="snapshot-list-empty">
<div class="snapshot-list-empty">No snapshot found ...</div>
</script>
<script type="text/template" id="snapshot-list-item">
<div class="snapshot-item" data-snapshot-id={{id}}>
<div class="snapshot-preview lowcont-dark-picker-background"></div>
<div class="snapshot-details">
<span class="snapshot-details-title">{{name}} {{description}}</span>
<span class="snapshot-details-info">Snapshot recorded {{date}}</span>
<span class="snapshot-details-info">{{frames}}, size {{resolution}}, {{fps}}fps</span>
</div>
<div class="snapshot-actions">
<button class="button button-primary" data-action="load" data-snapshot-id="{{id}}">Load</button>
</div>
</div>
</script>

View File

@@ -1,7 +1,13 @@
<script type="text/html" id="templates/settings/export/gif.html">
<div class="export-panel-gif">
<div class="export-panel-header export-info">
Convert your sprite to an animated GIF. Opacity will not be preserved. Colors might be resampled.
Convert your sprite to an animated GIF.
</div>
<div class="gif-export-warning">
<div class="gif-export-warning-icon icon-common-warning-red">&nbsp;</div>
<div class="gif-export-warning-message">
Too many colors: can not preserve original colors or transparency.
</div>
</div>
<div class="export-panel-section export-panel-row">
<button type="button" class="button button-primary gif-download-button">Download</button>

View File

@@ -38,16 +38,11 @@
<div class="settings-title">
Recover recent sessions
</div>
<div class="settings-item previous-session">
</div>
</div>
</script>
<script type="text/template" id="previous-session-info-template">
<div>
Restore a backup of <span class="highlight">{{name}}</span>, saved at <span style="color:white">{{date}}</span> ?
<div style="margin-top:10px;">
<button type="button" class="button button-primary restore-session-button">Restore</button>
<div class="settings-item">
Browse backups of previous sessions.
<div style="margin-top:10px;">
<button type="button" class="button button-primary browse-backups-button">Browse backups</button>
</div>
</div>
</div>
</script>

View File

@@ -39,7 +39,7 @@
<script type="text/template" id="save-gallery-unavailable-partial">
<div class="settings-title">Save online</div>
<div class="settings-item">
<div class="save-status">Login to <a href="http://piskelapp.com" target="_blank">piskelapp.com</a> to save and share your sprites online.</div>
<div class="save-status">Login to <a href="https://www.piskelapp.com" target="_blank">piskelapp.com</a> to save and share your sprites online.</div>
</div>
</script>

View File

@@ -8,7 +8,7 @@
"layers.duplicate.json",
"layers.fun.json",
"layers.merge.json",
"layers.top.bottom",
"layers.top.bottom.json",
"lighten.darken.json",
"move.json",
"move-alllayers-allframes.json",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,237 @@
describe('BackupDatabase test', function () {
// Test object.
var backupDatabase;
var _toSnapshot = function (session_id, name, description, date, serialized) {
return {
session_id: session_id,
name: name,
description: description,
date: date,
serialized: serialized
};
};
var _checkSnapshot = function (actual, expected) {
expect(actual.session_id).toBe(expected.session_id);
expect(actual.name).toBe(expected.name);
expect(actual.description).toBe(expected.description);
expect(actual.date).toBe(expected.date);
expect(actual.serialized).toBe(expected.serialized);
};
var _addSnapshots = function (snapshots) {
var _add = function (index) {
return backupDatabase.createSnapshot(snapshots[index]).then(function () {
if (snapshots[index + 1]) {
return _add(index + 1);
} else {
return Promise.resolve();
}
})
};
return _add(0);
};
beforeEach(function (done) {
// Drop the database before each test.
var dbName = pskl.database.BackupDatabase.DB_NAME;
var req = window.indexedDB.deleteDatabase(dbName);
req.onsuccess = done;
});
afterEach(function () {
// Close the database if it was still open.
if (backupDatabase && backupDatabase.db) {
backupDatabase.db.close();
}
});
it('initializes the DB and returns a promise', function (done) {
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init().then(done);
});
it('can add snapshots and retrieve them', function (done) {
var snapshot = _toSnapshot('session_1', 'name', 'desc', 0, 'serialized');
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function (db) {
// Create snapshot in backup database
return backupDatabase.createSnapshot(snapshot);
}).then(function () {
// Get snapshots for session_1 in backup database
return backupDatabase.getSnapshotsBySessionId('session_1');
}).then(function (snapshots) {
expect(snapshots.length).toBe(1);
_checkSnapshot(snapshots[0], snapshot);
done();
});
});
it('can update snapshots and retrieve them', function (done) {
var snapshot = _toSnapshot('session_1', 'name', 'desc', 0, 'serialized');
var updated = _toSnapshot('session_1', 'name_updated', 'desc_updated', 10, 'serialized_updated');
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function () {
// Create snapshot in backup database
return backupDatabase.createSnapshot(snapshot);
}).then(function () {
// Retrieve snapshots to get the inserted snapshot id
return backupDatabase.getSnapshotsBySessionId('session_1');
}).then(function (snapshots) {
// Update snapshot in backup database
updated.id = snapshots[0].id;
return backupDatabase.updateSnapshot(updated);
}).then(function () {
// Get snapshots for session_1 in backup database
return backupDatabase.getSnapshotsBySessionId('session_1');
}).then(function (snapshots) {
expect(snapshots.length).toBe(1);
_checkSnapshot(snapshots[0], updated);
done();
});
});
it('can delete snapshots', function (done) {
var testSnapshots = [
_toSnapshot('session_1', 'name1', 'desc1', 0, 'serialized1'),
_toSnapshot('session_1', 'name2', 'desc2', 0, 'serialized2'),
_toSnapshot('session_2', 'name3', 'desc3', 0, 'serialized3')
];
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function () {
return _addSnapshots(testSnapshots);
}).then(function () {
// Retrieve snapshots to get the inserted snapshot id
return backupDatabase.getSnapshotsBySessionId('session_1');
}).then(function (snapshots) {
expect(snapshots.length).toBe(2);
// Delete snapshot with 'name1' from backup database
var snapshot = snapshots.filter(function (s) { return s.name === 'name1' })[0];
return backupDatabase.deleteSnapshot(snapshot);
}).then(function () {
// Get snapshots for session_1 in backup database
return backupDatabase.getSnapshotsBySessionId('session_1');
}).then(function (snapshots) {
expect(snapshots.length).toBe(1);
_checkSnapshot(snapshots[0], testSnapshots[1]);
done();
});
});
it('returns an empty array when calling getSnapshots for an empty session', function (done) {
var testSnapshots = [
_toSnapshot('session_1', 'name1', 'desc1', 0, 'serialized1'),
_toSnapshot('session_1', 'name2', 'desc2', 0, 'serialized2'),
_toSnapshot('session_2', 'name3', 'desc3', 0, 'serialized3')
];
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function () {
return _addSnapshots(testSnapshots);
}).then(function () {
// Retrieve snapshots for a session that doesn't exist
return backupDatabase.getSnapshotsBySessionId('session_3');
}).then(function (snapshots) {
expect(snapshots.length).toBe(0);
done();
});
});
it('can delete all snapshots for a session', function (done) {
var testSnapshots = [
_toSnapshot('session_1', 'name1', 'desc1', 0, 'serialized1'),
_toSnapshot('session_1', 'name2', 'desc2', 0, 'serialized2'),
_toSnapshot('session_2', 'name3', 'desc3', 0, 'serialized3')
];
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function () {
return _addSnapshots(testSnapshots);
}).then(function () {
// Retrieve snapshots to get the inserted snapshot id
return backupDatabase.getSnapshotsBySessionId('session_1');
}).then(function (snapshots) {
// Check that we have 2 snapshots for session_1
expect(snapshots.length).toBe(2);
// Delete snapshots for session_1
return backupDatabase.deleteSnapshotsForSession('session_1');
}).then(function () {
// Get snapshots for session_1 in backup database
return backupDatabase.getSnapshotsBySessionId('session_1');
}).then(function (snapshots) {
// All snapshots should have been deleted
expect(snapshots.length).toBe(0);
// Get snapshots for session_2 in backup database
return backupDatabase.getSnapshotsBySessionId('session_2');
}).then(function (snapshots) {
// There should still be the snapshot for session_2
expect(snapshots.length).toBe(1);
_checkSnapshot(snapshots[0], testSnapshots[2]);
done();
});
});
it('does a noop when calling deleteAllSnapshotsForSession for a missing session', function (done) {
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function () {
// Delete snapshot with 'name1' from backup database
return backupDatabase.deleteSnapshotsForSession('session_1');
}).then(function () {
done();
});
});
it('returns sessions array when calling getSessions', function (done) {
var testSnapshots = [
_toSnapshot('session_1', 'name1', 'desc1', 5, 'serialized1'),
_toSnapshot('session_1', 'name2', 'desc2', 10, 'serialized2'),
_toSnapshot('session_2', 'name3', 'desc3', 15, 'serialized3')
];
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function () {
return _addSnapshots(testSnapshots);
}).then(function () {
return backupDatabase.getSessions();
}).then(function (sessions) {
// Check that we have 2 sessions
expect(sessions.length).toBe(2);
// Get the actual sessions
var session1 = sessions.filter(function (s) { return s.id === 'session_1'; })[0];
var session2 = sessions.filter(function (s) { return s.id === 'session_2'; })[0];
// Check the start/end date were computed properly
expect(session1.startDate).toBe(5);
expect(session1.endDate).toBe(10);
expect(session2.startDate).toBe(15);
expect(session2.endDate).toBe(15);
done();
});
});
it('returns an empty array when calling getSessions on an empty DB', function (done) {
backupDatabase = new pskl.database.BackupDatabase();
backupDatabase.init()
.then(function () {
return backupDatabase.getSessions();
}).then(function (sessions) {
expect(sessions.length).toBe(0);
done();
});
});
});

View File

@@ -0,0 +1,158 @@
describe('PiskelDatabase test', function () {
// Test object.
var piskelDatabase;
var _toSnapshot = function (session_id, name, description, date, serialized) {
return {
session_id: session_id,
name: name,
description: description,
date: date,
serialized: serialized
};
};
var _checkPiskel = function (actual, expected) {
expect(actual.name).toBe(expected[0]);
expect(actual.description).toBe(expected[1]);
expect(actual.date).toBe(expected[2]);
expect(actual.serialized).toBe(expected[3]);
};
var _addPiskels = function (piskels) {
var _add = function (index) {
var piskelData = piskels[index];
return piskelDatabase.create.apply(piskelDatabase, piskelData)
.then(function () {
if (piskels[index + 1]) {
return _add(index + 1);
} else {
return Promise.resolve();
}
});
};
return _add(0);
};
beforeEach(function (done) {
// Mock the migration script.
spyOn(pskl.database.migrate.MigrateLocalStorageToIndexedDb, "migrate");
// Drop the database before each test.
var dbName = pskl.database.PiskelDatabase.DB_NAME;
var req = window.indexedDB.deleteDatabase(dbName);
req.onsuccess = done;
});
afterEach(function () {
// Close the database if it was still open.
if (piskelDatabase && piskelDatabase.db) {
piskelDatabase.db.close();
}
});
it('initializes the DB and returns a promise', function (done) {
piskelDatabase = new pskl.database.PiskelDatabase();
piskelDatabase.init().then(done);
});
it('can add a piskel and retrieve it', function (done) {
piskelDatabase = new pskl.database.PiskelDatabase();
piskelDatabase.init()
.then(function (db) {
return piskelDatabase.create('name', 'desc', 0, 'serialized');
}).then(function () {
return piskelDatabase.get('name');
}).then(function (piskel) {
expect(piskel.name).toBe('name');
expect(piskel.description).toBe('desc');
expect(piskel.date).toBe(0);
expect(piskel.serialized).toBe('serialized');
done();
});
});
it('can delete piskel by name', function (done) {
var piskels = [
['n1', 'd1', 10, 's1'],
['n2', 'd2', 20, 's2'],
['n3', 'd3', 30, 's3'],
];
piskelDatabase = new pskl.database.PiskelDatabase();
piskelDatabase.init()
.then(function (db) {
return _addPiskels(piskels);
}).then(function () {
return piskelDatabase.delete('n2');
}).then(function () {
return piskelDatabase.get('n1');
}).then(function (piskelData) {
_checkPiskel(piskelData, piskels[0]);
return piskelDatabase.get('n3');
}).then(function (piskelData) {
_checkPiskel(piskelData, piskels[2]);
return piskelDatabase.get('n2');
}).then(function (piskelData) {
expect(piskelData).toBe(undefined);
done();
});
});
it('can list piskels', function (done) {
var piskels = [
['n1', 'd1', 10, 's1'],
['n2', 'd2', 20, 's2'],
['n3', 'd3', 30, 's3'],
];
piskelDatabase = new pskl.database.PiskelDatabase();
piskelDatabase.init()
.then(function (db) {
return _addPiskels(piskels);
}).then(function () {
return piskelDatabase.list();
}).then(function (piskels) {
expect(piskels.length).toBe(3);
piskels.forEach(function (piskelData) {
expect(piskelData.name).toMatch(/n[1-3]/);
expect(piskelData.description).toMatch(/d[1-3]/);
expect(piskelData.date).toBeDefined();
expect(piskelData.serialized).not.toBeDefined();
})
done();
});
});
it('can update piskel with same name', function (done) {
var piskels = [
['n1', 'd1', 10, 's1'],
['n2', 'd2', 20, 's2'],
['n3', 'd3', 30, 's3'],
];
piskelDatabase = new pskl.database.PiskelDatabase();
piskelDatabase.init()
.then(function (db) {
return _addPiskels(piskels);
}).then(function () {
return piskelDatabase.update('n2', 'd2_updated', 40, 's2_updated');
}).then(function (piskels) {
return piskelDatabase.list();
}).then(function (piskels) {
expect(piskels.length).toBe(3);
var p2 = piskels.filter(function (p) { return p.name === 'n2'})[0];
expect(p2.name).toBe('n2');
expect(p2.description).toBe('d2_updated');
expect(p2.date).toBe(40);
return piskelDatabase.get('n2');
}).then(function (piskel) {
_checkPiskel(piskel, ['n2', 'd2_updated', 40, 's2_updated']);
done();
});
});
});

View File

@@ -29,6 +29,19 @@ describe("SelectionManager suite", function() {
}
};
/**
* @Mock
*/
var createMockCopyEvent = function () {
return {
clipboardData: {
items: [],
setData: function () {}
},
preventDefault: function () {}
};
};
beforeEach(function() {
currentFrame = pskl.model.Frame.fromPixelGrid([
[B, R, T],
@@ -52,7 +65,7 @@ describe("SelectionManager suite", function() {
selectMiddleLine();
console.log('[SelectionManager] ... copy');
selectionManager.copy();
selectionManager.copy({ type: Events.CLIPBOARD_COPY }, createMockCopyEvent());
console.log('[SelectionManager] ... check selection content after copy contains correct colors');
expect(selection.pixels.length).toBe(3); // or not to be ... lalalala ... french-only joke \o/
@@ -69,7 +82,7 @@ describe("SelectionManager suite", function() {
checkContainsPixel(selection.pixels, 2, 2, R);
console.log('[SelectionManager] ... paste');
selectionManager.paste();
selectionManager.paste({ type: Events.CLIPBOARD_PASTE }, createMockCopyEvent());
console.log('[SelectionManager] ... check last line is identical to middle line after paste');
frameEqualsGrid(currentFrame, [
@@ -87,7 +100,7 @@ describe("SelectionManager suite", function() {
selectMiddleLine();
console.log('[SelectionManager] ... cut');
selectionManager.cut();
selectionManager.copy({ type: Events.CLIPBOARD_CUT }, createMockCopyEvent());
console.log('[SelectionManager] ... check middle line was cut in the source frame');
frameEqualsGrid(currentFrame, [
@@ -97,7 +110,7 @@ describe("SelectionManager suite", function() {
]);
console.log('[SelectionManager] ... paste');
selectionManager.paste();
selectionManager.paste({ type: Events.CLIPBOARD_PASTE }, createMockCopyEvent());
console.log('[SelectionManager] ... check middle line was restored by paste');
frameEqualsGrid(currentFrame, [
@@ -118,7 +131,7 @@ describe("SelectionManager suite", function() {
selection.move(2, 0);
console.log('[SelectionManager] ... copy out of bounds');
selectionManager.copy();
selectionManager.copy({ type: Events.CLIPBOARD_COPY }, createMockCopyEvent());
console.log('[SelectionManager] ... check out of bound pixels were replaced by transparent pixels');
checkContainsPixel(selection.pixels, 1, 2, R);
checkContainsPixel(selection.pixels, 1, 3, T);
@@ -133,7 +146,7 @@ describe("SelectionManager suite", function() {
checkContainsPixel(selection.pixels, 1, 3, T);
console.log('[SelectionManager] ... paste out of bounds');
selectionManager.paste();
selectionManager.paste({ type: Events.CLIPBOARD_PASTE }, createMockCopyEvent());
console.log('[SelectionManager] ... check pixel at (1,1) is red after paste');
frameEqualsGrid(currentFrame, [
@@ -154,7 +167,7 @@ describe("SelectionManager suite", function() {
selection.move(2, 0);
console.log('[SelectionManager] ... cut out of bounds');
selectionManager.cut();
selectionManager.copy({ type: Events.CLIPBOARD_CUT }, createMockCopyEvent());
console.log('[SelectionManager] ... check last pixel of midle line was cut in the source frame');
frameEqualsGrid(currentFrame, [
[B, R, T],
@@ -165,7 +178,7 @@ describe("SelectionManager suite", function() {
selection.move(-1, 0);
console.log('[SelectionManager] ... paste out of bounds');
selectionManager.paste();
selectionManager.paste({ type: Events.CLIPBOARD_PASTE }, createMockCopyEvent());
console.log('[SelectionManager] ... check middle line final state');
frameEqualsGrid(currentFrame, [

View File

@@ -0,0 +1,286 @@
describe('BackupService test', function () {
// Some helper const.
var ONE_SECOND = 1000;
var ONE_MINUTE = 60 * ONE_SECOND;
var mockBackupDatabase;
var mockPiskel;
var mockPiskelController;
// Globals used in stubs
var stubValues = {
snapshotDate: null,
serializedPiskel: null
};
// Main test object.
var backupService;
beforeEach(function () {
// Create mocks.
mockBackupDatabase = {
// Test property
_sessions: {},
init: function () {},
getSnapshotsBySessionId: function (sessionId) {
// Default implementation that looks up in _sessions or returns an
// empty array.
return Promise.resolve(this._sessions[sessionId] || []);
},
updateSnapshot: function () { return Promise.resolve(); },
createSnapshot: function () { return Promise.resolve(); },
deleteSnapshot: function () { return Promise.resolve(); },
getSessions: function () { return Promise.resolve([]); },
deleteSnapshotsForSession: function () { return Promise.resolve(); },
findLastSnapshot: function () { return Promise.resolve(null); }
};
mockPiskel = {
_descriptor: {},
_hash: null,
getDescriptor: function () { return this._descriptor; },
getHash: function () { return this._hash; },
getWidth: function () { return 32; },
getHeight: function () { return 32; },
getFrameCount: function () { return 1; },
getFPS: function () { return 12; },
};
mockPiskelController = {
getPiskel: function () { return mockPiskel; },
setPiskel: function () {}
};
spyOn(pskl.utils.serialization.Serializer, 'serialize').and.callFake(function () {
return stubValues.serializedPiskel;
});
// Create test backup service with mocks.
backupService = new pskl.service.BackupService(
mockPiskelController,
mockBackupDatabase
);
// Override the currentDate_ internal helper in order to set
// custom snapshot dates.
backupService.currentDate_ = function () {
return snapshotDate;
}
});
var createSnapshotObject = function (session_id, name, description, date, serialized) {
return {
session_id: session_id,
name: name,
description: description,
date: date,
serialized: serialized
};
};
var preparePiskelMocks = function (session_id, name, description, hash, serialized) {
// Update the session id.
pskl.app.sessionId = session_id;
// Update the piskel mock.
mockPiskel._descriptor.name = name;
mockPiskel._descriptor.description = description;
mockPiskel._hash = hash;
stubValues.serializedPiskel = serialized;
};
it('calls create to backup', function (done) {
preparePiskelMocks(1, 'piskel_name', 'piskel_desc', 'piskel_hash', 'serialized');
// Set snashot date.
snapshotDate = 5;
// No snapshots currently saved.
spyOn(mockBackupDatabase, 'createSnapshot').and.callThrough();
backupService.backup().then(function () {
expect(mockBackupDatabase.createSnapshot).toHaveBeenCalled();
var snapshot = mockBackupDatabase.createSnapshot.calls.mostRecent().args[0]
expect(snapshot.session_id).toEqual(1);
expect(snapshot.name).toEqual('piskel_name');
expect(snapshot.description).toEqual('piskel_desc');
expect(snapshot.date).toEqual(5);
expect(snapshot.serialized).toEqual('serialized');
done();
});
});
it('does not call update to backup if the hash did not change', function (done) {
var session = 1;
var date1 = 0;
var date2 = ONE_MINUTE;
var snapshot1 = createSnapshotObject(1, 'piskel_name1', 'piskel_desc1', date1, 'serialized1');
preparePiskelMocks(session, 'piskel_name1', 'piskel_desc1', 'hash', 'serialized1');
snapshotDate = date1;
// Prepare spies.
spyOn(mockBackupDatabase, 'updateSnapshot').and.callThrough();
spyOn(mockBackupDatabase, 'createSnapshot').and.callThrough();
backupService.backup().then(function () {
// The snapshot should have been created using "createSnapshot".
expect(mockBackupDatabase.createSnapshot).toHaveBeenCalled();
expect(mockBackupDatabase.updateSnapshot.calls.any()).toBe(false);
// Prepare snapshot1 to be returned in the list of already existing sessions.
mockBackupDatabase._sessions[session] = [snapshot1];
preparePiskelMocks(session, 'piskel_name2', 'piskel_desc2', 'hash', 'serialized2');
snapshotDate = date2;
backupService.backup().then(function () {
// Check that createSnapshot was not called again and updateSnapshot either.
expect(mockBackupDatabase.createSnapshot.calls.count()).toEqual(1);
expect(mockBackupDatabase.updateSnapshot.calls.count()).toEqual(0);
done();
});
});
});
it('calls update to backup if there is an existing & recent snapshot', function (done) {
var session = 1;
var date1 = 0;
var date2 = ONE_MINUTE;
var snapshot1 = createSnapshotObject(1, 'piskel_name1', 'piskel_desc1', date1, 'serialized1');
preparePiskelMocks(session, 'piskel_name1', 'piskel_desc1', 'piskel_hash1', 'serialized1');
snapshotDate = date1;
// Prepare spies.
spyOn(mockBackupDatabase, 'updateSnapshot').and.callThrough();
spyOn(mockBackupDatabase, 'createSnapshot').and.callThrough();
backupService.backup().then(function () {
// The snapshot should have been created using "createSnapshot".
expect(mockBackupDatabase.createSnapshot).toHaveBeenCalled();
// Prepare snapshot1 to be returned in the list of already existing sessions.
mockBackupDatabase._sessions[session] = [snapshot1];
preparePiskelMocks(session, 'piskel_name2', 'piskel_desc2', 'piskel_hash2', 'serialized2');
snapshotDate = date2;
backupService.backup().then(function () {
// Check that createSnapshot was not called again.
expect(mockBackupDatabase.createSnapshot.calls.count()).toEqual(1);
// Check that updateSnapshot was called with the expected arguments.
expect(mockBackupDatabase.updateSnapshot).toHaveBeenCalled();
var snapshot = mockBackupDatabase.updateSnapshot.calls.mostRecent().args[0]
expect(snapshot.session_id).toEqual(session);
expect(snapshot.name).toEqual('piskel_name2');
expect(snapshot.description).toEqual('piskel_desc2');
expect(snapshot.date).toEqual(date2);
expect(snapshot.serialized).toEqual('serialized2');
done();
});
});
});
it('creates a new snapshot if the time difference is big enough', function (done) {
var session = 1;
var date1 = 0;
var date2 = 6 * ONE_MINUTE;
var snapshot1 = createSnapshotObject(1, 'piskel_name1', 'piskel_desc1', date1, 'serialized1');
preparePiskelMocks(session, 'piskel_name1', 'piskel_desc1', 'piskel_hash1', 'serialized1');
snapshotDate = date1;
// Prepare spies.
spyOn(mockBackupDatabase, 'updateSnapshot').and.callThrough();
spyOn(mockBackupDatabase, 'createSnapshot').and.callThrough();
backupService.backup().then(function () {
// The snapshot should have been created using "createSnapshot".
expect(mockBackupDatabase.createSnapshot).toHaveBeenCalled();
// Prepare snapshot1 to be returned in the list of already existing sessions.
mockBackupDatabase._sessions[session] = [snapshot1];
preparePiskelMocks(session, 'piskel_name2', 'piskel_desc2', 'piskel_hash2', 'serialized2');
snapshotDate = date2;
backupService.backup().then(function () {
// Check that updateSnapshot was not called.
expect(mockBackupDatabase.updateSnapshot.calls.count()).toEqual(0);
// Check that updateSnapshot was called with the expected arguments.
expect(mockBackupDatabase.createSnapshot).toHaveBeenCalled();
var snapshot = mockBackupDatabase.createSnapshot.calls.mostRecent().args[0]
expect(snapshot.session_id).toEqual(session);
expect(snapshot.name).toEqual('piskel_name2');
expect(snapshot.description).toEqual('piskel_desc2');
expect(snapshot.date).toEqual(date2);
expect(snapshot.serialized).toEqual('serialized2');
done();
});
});
});
it('deletes old snapshots if there are too many of them', function (done) {
var session = 1;
var maxPerSession = 12;
preparePiskelMocks(session, 'piskel_name', 'piskel_desc', 'piskel_hash', 'serialized12');
snapshotDate = 12 * 6 * ONE_MINUTE;
// Prepare spies.
spyOn(mockBackupDatabase, 'deleteSnapshot').and.callThrough();
spyOn(mockBackupDatabase, 'createSnapshot').and.callThrough();
// Prepare array of already saved snapshots.
mockBackupDatabase._sessions[session] = [];
for (var i = maxPerSession - 1 ; i >= 0 ; i--) {
mockBackupDatabase._sessions[session].push(
createSnapshotObject(session, 'piskel_name', 'piskel_desc', i * 6 * ONE_MINUTE, 'serialized' + i)
);
}
backupService.backup().then(function () {
expect(mockBackupDatabase.createSnapshot).toHaveBeenCalled();
expect(mockBackupDatabase.deleteSnapshot).toHaveBeenCalled();
// It will simply attempt to delete the last item from the array of saved sessions
var snapshot = mockBackupDatabase.deleteSnapshot.calls.mostRecent().args[0];
expect(snapshot.session_id).toEqual(session);
expect(snapshot.name).toEqual('piskel_name');
expect(snapshot.description).toEqual('piskel_desc');
expect(snapshot.date).toEqual(0);
expect(snapshot.serialized).toEqual('serialized0');
done();
});
});
it('deletes a session if there are too many of them', function (done) {
var session = 'session10';
var maxSessions = 10;
preparePiskelMocks(session, 'piskel_name', 'piskel_desc', 'piskel_hash', 'serialized12');
snapshotDate = 10 * ONE_MINUTE;
// Prepare array of sessions.
var sessions = [];
for (var i = 0 ; i < maxSessions + 1 ; i++) {
sessions.push({
id: 'session' + i,
startDate: i * ONE_MINUTE
});
}
// Prepare spies.
spyOn(mockBackupDatabase, 'getSessions').and.returnValue(Promise.resolve(sessions));
spyOn(mockBackupDatabase, 'createSnapshot').and.callThrough();
spyOn(mockBackupDatabase, 'deleteSnapshotsForSession').and.callThrough();
backupService.backup().then(function () {
expect(mockBackupDatabase.createSnapshot).toHaveBeenCalled();
expect(mockBackupDatabase.deleteSnapshotsForSession).toHaveBeenCalled();
// It will simply attempt to delete the last item from the array of saved sessions
var sessionId = mockBackupDatabase.deleteSnapshotsForSession.calls.mostRecent().args[0];
expect(sessionId).toEqual('session0');
done();
});
});
});

View File

@@ -2,7 +2,7 @@ describe("Palette Service", function() {
var paletteService = null;
var localStorage = {};
var localStorageService;
var localStorageGlobal;
var addPalette = function (id, name, color) {
@@ -24,7 +24,7 @@ describe("Palette Service", function() {
beforeEach(function() {
localStorage = {};
localStorageService = {
localStorageGlobal = {
getItem : function (key) {
if (localStorage.hasOwnProperty(key)) {
return localStorage[key];
@@ -38,21 +38,21 @@ describe("Palette Service", function() {
};
paletteService = new pskl.service.palette.PaletteService();
paletteService.localStorageService = localStorageService;
paletteService.localStorageGlobal = localStorageGlobal;
});
it("returns an empty array when no palette is stored", function() {
spyOn(localStorageService, 'getItem').and.callThrough();
spyOn(localStorageGlobal, 'getItem').and.callThrough();
var palettes = paletteService.getPalettes();
expect(Array.isArray(palettes)).toBe(true);
expect(palettes.length).toBe(0);
expect(localStorageService.getItem).toHaveBeenCalled();
expect(localStorageGlobal.getItem).toHaveBeenCalled();
});
it("can store a palette", function() {
// when
spyOn(localStorageService, 'setItem').and.callThrough();
spyOn(localStorageGlobal, 'setItem').and.callThrough();
var paletteId = 'palette-id';
var paletteName = 'palette-name';
@@ -63,7 +63,7 @@ describe("Palette Service", function() {
var palettes = paletteService.getPalettes();
// verify
expect(localStorageService.setItem).toHaveBeenCalled();
expect(localStorageGlobal.setItem).toHaveBeenCalled();
expect(Array.isArray(palettes)).toBe(true);
expect(palettes.length).toBe(1);