56 Commits

Author SHA1 Message Date
a69b44eaec Return promise results from PiskelDB and IndexedDBStorageService 2017-10-17 02:11:08 +02:00
3e779a651f Show error message if BackupDatabase promise rejected 2017-10-17 02:09:28 +02:00
ab9bbce1ed Add templates for backup database errors 2017-10-17 02:08:48 +02:00
cfd3773a2b Issue #751 - add repeat checkbox to GIF export panel 2017-10-08 19:46:43 +02:00
0eface45f1 Issue #750 - drawing tests: always initialize penSize before starting test 2017-10-08 19:12:04 +02:00
87893bb4ac Issue #750 - Fix mirror pen with even pensizes 2017-10-08 19:12:04 +02:00
77d26bffa9 Issue #727 - add integration test for simple import flow 2017-10-08 18:19:52 +02:00
652027bd3f Issue #727 - update integration tests to wait for color service update 2017-10-08 18:19:52 +02:00
bf4cc3302a Issue #727 - skip import steps if current piskel is empty 2017-10-08 18:19:52 +02:00
95c8df1224 Issue #727 - simplify import: resize and insertion steps 2017-10-08 18:19:52 +02:00
7445357368 Issue #727 - simplify import mode text 2017-10-08 18:19:52 +02:00
a2369cac0c Issue #727 - remove border around meta info in import wizard 2017-10-08 18:19:52 +02:00
51538dff48 Make piskel performance warning less scary 2017-09-24 18:06:37 +02:00
da739e78da Issue #743 - bump color palette cap to 256 2017-09-24 17:39:03 +02:00
dd8217e21b Issue #744 - show notification when exporting to GIF can not preserve colors 2017-09-24 17:37:49 +02:00
d502d3416b Issue #745 - Add https support 2017-09-24 17:37:14 +02:00
d1156954ca Issue #729 - implement custom PNG export viewer instead of opening window to data-uri 2017-09-24 17:36:02 +02:00
dc5209628c fix selectionmanager unit test 2017-09-06 23:05:17 +02:00
8568663949 Move clipboard events to dedicated service and fix tests 2017-09-06 23:05:17 +02:00
fd3d828067 remove unused selection copy cut paste events 2017-09-06 23:05:17 +02:00
e1797b2008 Fix SelectionManagerTest by using a clipboard event mock 2017-09-06 23:05:17 +02:00
0a43f6bbec Fix copy to website script to work if main-partial is missing. 2017-09-06 23:05:17 +02:00
b9423bc831 Issue #645: Support clipboard to paste images 2017-09-06 23:05:17 +02:00
5e6280301d Issue #736 - cleanup selection tool state on SELECTION_DISMISSED event 2017-09-06 00:39:35 +02:00
5671eb4782 Delete all extra backup sessions if MAX is reached 2017-08-06 22:56:43 +02:00
35788b54ba update travis yml to upgrade node and stop downloading casper 2017-08-03 00:44:53 +02:00
629ecf83b4 add comments for values synced between JS and CSS 2017-08-03 00:21:08 +02:00
c037b07693 rename mergeData to backupsData in browse backups wizard 2017-08-03 00:21:08 +02:00
c31b7a351c update piskel mock in BackupServiceTest 2017-08-03 00:21:08 +02:00
7de03f1e73 show snpashot previews in the browse backups dialog 2017-08-03 00:21:08 +02:00
eab21e0839 Show confirmation message when loading snapshot backup 2017-08-03 00:21:08 +02:00
2b3bd02479 improve styling of snapshot list in browse backups dialog 2017-08-03 00:21:08 +02:00
4e86fa1570 dev-environment: add ctrl+alt+R shortcut to reload styles 2017-08-03 00:21:08 +02:00
170a7e4731 skip backups for current session in browse backups dialog 2017-08-03 00:21:08 +02:00
6b7f04b63e browse backups dialog: add styling for empty session list 2017-08-03 00:21:08 +02:00
da2e9f99e4 cleanup: remove title on backup session element 2017-08-03 00:21:08 +02:00
530a949e54 add icon for backup dialog 2017-08-03 00:21:08 +02:00
4377c9e601 add disclaimer in the browse backups dialog 2017-08-03 00:21:08 +02:00
e0bbb88d47 confirm backup session delete, add animation 2017-08-03 00:21:08 +02:00
9ff2ecbb45 improve styling for browse-backups dialog 2017-08-03 00:21:08 +02:00
8beba2088b remove useless console.log 2017-08-03 00:21:08 +02:00
ee45cdcc45 add a browse backups dialog 2017-08-03 00:21:08 +02:00
30ea7fa079 fix migration script for localstorage to indexeddb 2017-08-03 00:21:08 +02:00
e9b39a5c61 add unit test for PiskelDatabase 2017-08-03 00:21:08 +02:00
d0a32b18c5 add unit test for backup database 2017-08-03 00:21:08 +02:00
372ad1f513 add unit test for BackupService 2017-08-03 00:21:08 +02:00
c6e106fe2d add a limit to the number of sessions backed up 2017-08-03 00:21:08 +02:00
f9570ea3c5 Issue #640 - extract database code to dedicated package 2017-08-03 00:21:08 +02:00
f9cb631acb Issue #640 - migrate backup service to indexeddb 2017-08-03 00:21:08 +02:00
ed749a747f Issue #640 - migrate local browser save to indexeddb 2017-08-03 00:21:08 +02:00
30ecd41452 Issue #640 - remove duplicated entries in piskel-script-list 2017-08-03 00:21:08 +02:00
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
183133496e Fix #718 - when dropping image, only use import wizard for big images 2017-08-01 01:06:09 +02:00
8a2c0191f9 release: bump version to 0.12.1 2017-07-18 08:06:54 +02:00
a096dcabfd Fix #717: filter invalid colors 2017-07-18 08:05:48 +02:00
96d326ef12 release: bump version to 0.13.0-SNAPSHOT 2017-06-23 21:01:47 +02:00
76 changed files with 4215 additions and 248 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

@ -1,6 +1,6 @@
{
"name": "piskel",
"version": "0.12.0",
"version": "0.12.1",
"description": "Pixel art editor",
"author": "Julian Descottes <julian.descottes@gmail.com>",
"contributors": [

View File

@ -0,0 +1,152 @@
#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 .centered-message {
position: absolute;
left: 50%;
width: 200px;
margin-top: 100px;
margin-left: -130px;
padding: 30px;
font-size: 16px;
text-align: center;
border: 1px solid;
}
.browse-backups .session-list-empty,
.browse-backups .snapshot-list-empty {
color: #bbb;
}
.browse-backups .session-list-error,
.browse-backups .snapshot-list-error {
color: white;
}
.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

@ -183,22 +183,14 @@
.import-meta-value,
.import-meta-label {
padding: 2px 4px;
border: 1px solid gold;
}
.import-meta-label {
border-radius: 2px 0 0 2px;
color: var(--highlight-color);
border-right-width: 0;
}
.import-meta-title .import-meta-label {
border-right-width: 1px;
border-radius: 2px;
}
.import-meta-value {
border-radius: 0 2px 2px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -242,7 +234,7 @@
.insert-mode-option {
display: flex;
align-items: center;
margin-bottom: 5px;
margin: 5px 0;
}
.import-resize-option :checked + span,
@ -250,11 +242,15 @@
color: var(--highlight-color);
}
.import-resize-option input,
.insert-mode-option input {
margin: 5px;
}
/**
* ADJUST SIZE
*/
.import-resize-anchor-info,
.import-resize-option-label {
.import-resize-anchor-info {
margin-bottom: 10px;
}

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,6 +18,9 @@
*/
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();
@ -114,6 +117,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 +174,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 +204,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

@ -54,7 +54,9 @@
var colors = this.getSelectedPaletteColors_();
if (colors.length > 0) {
var html = colors.map(function (color, index) {
var html = colors.filter(function (color) {
return !!color;
}).map(function (color, index) {
return pskl.utils.Template.replace(this.paletteColorTemplate_, {
color : color,
index : index + 1,

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,101 @@
(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 = this.getMarkupForSessions_(sessions);
this.container.querySelector('.session-list').innerHTML = html;
}.bind(this)).catch(function () {
var html = pskl.utils.Template.get('session-list-error');
this.container.querySelector('.session-list').innerHTML = html;
}.bind(this));
};
ns.SelectSession.prototype.getMarkupForSessions_ = function (sessions) {
if (sessions.length === 0) {
return pskl.utils.Template.get('session-list-empty');
}
var sessionItemTemplate = pskl.utils.Template.get('session-list-item');
return sessions.reduce(function (previous, session) {
if (session.id === pskl.app.sessionId) {
// Do not show backups for the current session.
return previous;
}
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'
};
return previous + pskl.utils.Template.replace(sessionItemTemplate, view);
}, '');
};
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,102 @@
(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 = this.getMarkupForSnapshots_(snapshots);
this.container.querySelector('.snapshot-list').innerHTML = html;
// Load the image of the first frame for each sprite and update the list.
snapshots.forEach(function (snapshot) {
this.updateSnapshotPreview_(snapshot);
}.bind(this));
}.bind(this)).catch(function () {
var html = pskl.utils.Template.get('snapshot-list-error');
this.container.querySelector('.snapshot-list').innerHTML = html;
}.bind(this));
};
ns.SessionDetails.prototype.getMarkupForSnapshots_ = function (snapshots) {
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');
return pskl.utils.Template.get('snapshot-list-empty');
}
var sessionItemTemplate = pskl.utils.Template.get('snapshot-list-item');
return snapshots.reduce(function (previous, 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
};
return previous + pskl.utils.Template.replace(sessionItemTemplate, view);
}, '');
};
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

@ -80,7 +80,13 @@
var step = this.wizard.getCurrentStep();
if (step.name === 'IMAGE_IMPORT') {
this.wizard.goTo('SELECT_MODE');
if (this.piskelController.isEmpty()) {
// If the current sprite is empty finalize immediately and replace the current sprite.
this.mergeData.importMode = ns.steps.SelectMode.MODES.REPLACE;
this.finalizeImport_();
} else {
this.wizard.goTo('SELECT_MODE');
}
} else if (step.name === 'SELECT_MODE') {
if (this.mergeData.importMode === ns.steps.SelectMode.MODES.REPLACE) {
this.finalizeImport_();
@ -144,9 +150,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

@ -69,16 +69,15 @@
var anchorInfo = this.container.querySelector('.import-resize-anchor-info');
if (isBigger && keep) {
anchorInfo.innerHTML = [
'<span class="import-resize-warning">',
'<div class="import-resize-warning">',
' Imported content will be cropped!',
'</span>',
' ',
'Select crop origin'
'</div>',
'Select crop anchor:'
].join('');
} else if (isBigger) {
anchorInfo.innerHTML = 'Select the anchor for resizing the canvas';
anchorInfo.innerHTML = 'Select resize anchor:';
} else {
anchorInfo.innerHTML = 'Select where the import should be positioned';
anchorInfo.innerHTML = 'Select position anchor:';
}
};

View File

@ -42,12 +42,17 @@
this.addEventListener(this.frameOffsetY, 'keyup', this.onFrameInputKeyUp_);
pskl.utils.FileUtils.readImageFile(this.file_, this.onImageLoaded_.bind(this));
if (this.piskelController.isEmpty()) {
this.nextButton.textContent = 'import';
}
};
ns.ImageImport.prototype.onNextClick = function () {
this.container.classList.add('import-image-loading');
this.createPiskelFromImage().then(function (piskel) {
this.mergeData.mergePiskel = piskel;
this.container.classList.remove('import-image-loading');
this.superclass.onNextClick.call(this);
}.bind(this)).catch(function (e) {
console.error(e);
@ -257,9 +262,7 @@
context.lineTo(maxWidth * scaleX, y * scaleY);
}
// Set the line style to dashed
context.lineWidth = 1;
// context.setLineDash([2, 1]);
context.strokeStyle = 'gold';
context.stroke();

View File

@ -297,4 +297,12 @@
ns.PiskelController.prototype.serialize = function () {
return pskl.utils.serialization.Serializer.serialize(this.piskel);
};
/**
* Check if the current sprite is empty. Emptiness here means no pixel has been filled
* on any layer or frame for the current sprite.
*/
ns.PiskelController.prototype.isEmpty = function () {
return pskl.app.currentColorsService.getCurrentColors().length === 0;
};
})();

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,14 +14,22 @@
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');
this.downloadButton = document.querySelector('.gif-download-button');
this.repeatCheckbox = document.querySelector('.gif-repeat-checkbox');
// Initialize repeatCheckbox state
this.repeatCheckbox.checked = this.getRepeatSetting_();
this.addEventListener(this.uploadButton, 'click', this.onUploadButtonClick_);
this.addEventListener(this.downloadButton, 'click', this.onDownloadButtonClick_);
this.addEventListener(this.repeatCheckbox, 'change', this.onRepeatCheckboxChange_);
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 () {
@ -105,6 +113,7 @@
width: width * zoom,
height: height * zoom,
preserveColors : preserveColors,
repeat: this.getRepeatSetting_() ? 0 : 1,
transparent : transparent
});
@ -149,6 +158,15 @@
return transparentColor;
};
ns.GifExportController.prototype.onRepeatCheckboxChange_ = function () {
var checked = this.repeatCheckbox.checked;
pskl.UserSettings.set(pskl.UserSettings.EXPORT_GIF_REPEAT, checked);
};
ns.GifExportController.prototype.getRepeatSetting_ = function () {
return pskl.UserSettings.get(pskl.UserSettings.EXPORT_GIF_REPEAT);
};
ns.GifExportController.prototype.updateStatus_ = function (imageUrl, error) {
if (imageUrl) {
var linkTpl = '<a class="highlight" href="{{link}}" target="_blank">{{shortLink}}</a>';

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

@ -32,6 +32,7 @@
};
ns.DrawingTestPlayer.prototype.setupInitialState_ = function () {
var size = this.initialState.size;
var piskel = this.createPiskel_(size.width, size.height);
pskl.app.piskelController.setPiskel(piskel);
@ -39,9 +40,10 @@
$.publish(Events.SELECT_PRIMARY_COLOR, [this.initialState.primaryColor]);
$.publish(Events.SELECT_SECONDARY_COLOR, [this.initialState.secondaryColor]);
$.publish(Events.SELECT_TOOL, [this.initialState.selectedTool]);
if (this.initialState.penSize) {
pskl.app.penSizeService.setPenSize(this.initialState.penSize);
}
// Old tests do not have penSize stored in initialState, fallback to 1.
var penSize = this.initialState.penSize || 1;
pskl.app.penSizeService.setPenSize(this.initialState.penSize);
};
ns.DrawingTestPlayer.prototype.createPiskel_ = function (width, height) {
@ -108,6 +110,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 +173,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

@ -84,6 +84,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

@ -16,7 +16,7 @@
*/
ns.PerformanceReport = function (piskel, colorsCount) {
var pixels = piskel.getWidth() * piskel.getHeight();
this.resolution = pixels > (500 * 500);
this.resolution = pixels > (512 * 512);
var layersCount = piskel.getLayers().length;
this.layers = layersCount > 25;
@ -24,10 +24,10 @@
var framesCount = piskel.getLayerAt(0).size();
this.frames = framesCount > 100;
this.colors = colorsCount > 100;
this.colors = colorsCount >= 256;
var overallScore = (pixels / 2500) + (layersCount * 4) + framesCount + colorsCount;
this.overall = overallScore > 100;
var overallScore = (pixels / 2620) + (layersCount * 4) + framesCount + (colorsCount * 100 / 256);
this.overall = overallScore > 200;
};
ns.PerformanceReport.prototype.equals = function (report) {

View File

@ -0,0 +1,57 @@
(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().catch(function (e) {
console.log('Failed to initialize PiskelDatabase, local browser saves will be unavailable.');
});
};
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) {
return 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) {
return 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

@ -44,10 +44,17 @@
};
ns.VerticalMirrorPen.prototype.getSymmetricCol_ = function(col, frame) {
return frame.getWidth() - col - 1;
return frame.getWidth() - col - this.getPenSizeOffset_();
};
ns.VerticalMirrorPen.prototype.getSymmetricRow_ = function(row, frame) {
return frame.getHeight() - row - 1;
return frame.getHeight() - row - this.getPenSizeOffset_();
};
/**
* Depending on the pen size, the mirrored index need to have an offset of 1 pixel.
*/
ns.VerticalMirrorPen.prototype.getPenSizeOffset_ = function(row, frame) {
return pskl.app.penSizeService.getPenSize() % 2;
};
})();

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

@ -17,6 +17,7 @@
LAYER_OPACITY : 'LAYER_OPACITY',
EXPORT_SCALE: 'EXPORT_SCALE',
EXPORT_TAB: 'EXPORT_TAB',
EXPORT_GIF_REPEAT: 'EXPORT_GIF_REPEAT',
PEN_SIZE : 'PEN_SIZE',
RESIZE_SETTINGS: 'RESIZE_SETTINGS',
COLOR_FORMAT: 'COLOR_FORMAT',
@ -41,6 +42,7 @@
'LAYER_PREVIEW' : true,
'EXPORT_SCALE' : 1,
'EXPORT_TAB' : 'gif',
'EXPORT_GIF_REPEAT' : true,
'PEN_SIZE' : 1,
'RESIZE_SETTINGS': {
maintainRatio : true,

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",
@ -195,15 +204,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,83 @@
<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="centered-message session-list-empty">No session found ...</div>
</script>
<script type="text/template" id="session-list-error">
<div class="centered-message session-list-error">Could not load backup sessions, something went wrong.</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="centered-message snapshot-list-empty">No snapshot found ...</div>
</script>
<script type="text/template" id="snapshot-list-error">
<div class="centered-message snapshot-list-error">Could not load snapshots, something went wrong.</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

@ -67,11 +67,11 @@
<div class="import-mode">
<div class="import-mode-title">How do you want to import the new content?</div>
<div class="import-mode-section">
<span class="import-mode-section-description">Combine the imported content and your sprite.</span>
<button class="import-mode-merge-button button-primary button">Merge</button>
<span class="import-mode-section-description">Combine with your sprite</span>
<button class="import-mode-merge-button button-primary button">Combine</button>
</div>
<div class="import-mode-section">
<span class="import-mode-section-description">Replace your current sprite by the imported content.</span>
<span class="import-mode-section-description">Replace your sprite</span>
<button class="import-mode-replace-button button-primary button">Replace</button>
</div>
</div>
@ -131,9 +131,6 @@
The imported image is bigger than the current sprite.
</div>
<div class="import-resize-section">
<div class="import-resize-option-label">
How do you want to proceed?
</div>
<label class="import-resize-option">
<input type="radio" name="resize-mode" id="resize-option-expand" value="expand" {{expandChecked}}/>
<span>Expand canvas to {{newSize}}</span>
@ -162,19 +159,19 @@
</div>
<div class="import-step-content">
<div>Select a frame in your current sprite:</div>
<div>Select the insertion frame:</div>
<div class="insert-frame-preview"></div>
<div class="insert-mode-container">
<div class="insert-mode-option-label">
How should the imported frames be inserted:
Insert imported frames:
</div>
<label class="insert-mode-option">
<input type="radio" name="insert-mode" id="insert-mode-add" value="add" checked="checked"/>
<span>Add new frames</span>
<span>as new frames</span>
</label>
<label class="insert-mode-option">
<input type="radio" name="insert-mode" id="insert-mode-insert" value="insert"/>
<span>Insert in existing frames</span>
<span>in existing frames</span>
</label>
</div>
<div class="import-step-buttons">
@ -184,13 +181,3 @@
</div>
</div>
</script>
<script type="text/template" id="import-invalid-file">
<div class="import-step-container">
<div>THIS IS AN INVALID FILEZ</div>
<div class="import-step-buttons">
<button class="import-back-button button">back</button>
<button class="import-next-button button button-primary">next</button>
</div>
</div>
</script>

View File

@ -15,10 +15,10 @@
<p>If you ignore this warning, please save often!</p>
<p>To fix the issue, try adjusting your sprite settings:</p>
<ul>
<li>sprite resolution <sup title="recommended: lower than 256x256, max: 512x512" rel="tooltip" data-placement="top">?</sup></li>
<li>number of layers <sup title="recommended: lower than 5, max: 20" rel="tooltip" data-placement="top">?</sup></li>
<li>number of frames <sup title="recommended: lower than 25, max: 100" rel="tooltip" data-placement="top">?</sup></li>
<li>number of colors <sup title="max: 100" rel="tooltip" data-placement="top">?</sup></li>
<li>sprite resolution <sup title="recommended: lower than 512x512" rel="tooltip" data-placement="top">?</sup></li>
<li>number of layers <sup title="recommended: less than 10" rel="tooltip" data-placement="top">?</sup></li>
<li>number of frames <sup title="recommended: less than 50" rel="tooltip" data-placement="top">?</sup></li>
<li>number of colors <sup title="recommended: less than 256" rel="tooltip" data-placement="top">?</sup></li>
</ul>
<p>We strive to improve Piskel, its performance and stability, but this is a personal project with limited time and resources.
We prefer to warn you early rather than having you lose your work.</p>

View File

@ -1,7 +1,17 @@
<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">
<input id="gif-repeat-checkbox" class="gif-repeat-checkbox checkbox-fix" type="checkbox" />
<label for="gif-repeat-checkbox">Loop repeatedly</label>
</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

@ -7,6 +7,7 @@
'settings/test-export-png.js',
'settings/test-export-png-scale.js',
'settings/test-import-image.js',
'settings/test-import-image-empty.js',
'settings/test-import-image-twice.js',
'settings/test-resize-complete.js',
'settings/test-resize-content-complete.js',

View File

@ -9,7 +9,7 @@ casper.test.begin('PNG export test', 13, function(test) {
test.assert(!isDrawerExpanded(), 'settings drawer is closed');
// Setup test Piskel
// Setup test Piskel
setPiskelFromGrid('['+
'[B, T],' +
'[T, B],' +

View File

@ -0,0 +1,105 @@
/* globals casper, setPiskelFromGrid, isDrawerExpanded, getValue, isChecked,
evalLine, waitForEvent, replaceFunction, setPiskelFromImageSrc */
casper.test.begin('Image import test with an empty current sprite', 16, function(test) {
test.timeout = test.fail.bind(test, ['Test timed out']);
// Helper to retrieve the text content of the provided selector
// in the current wizard step.
var getTextContent = function (selector) {
return evalLine('document.querySelector(".current-step ' + selector +'").textContent');
};
function onTestStart() {
test.assertExists('#drawing-canvas-container canvas', 'Piskel ready, test starting');
test.assert(!isDrawerExpanded(), 'settings drawer is closed');
waitForEvent('PISKEL_RESET', onPiskelReset, test.timeout);
// 1x1 transparent pixel
setPiskelFromGrid('['+
'[T]' +
']');
}
function onPiskelReset() {
// Check the expected piskel was correctly loaded.
test.assertEquals(evalLine('pskl.app.currentColorsService.getCurrentColors().length'), 0, 'Has no color');
test.assertEquals(evalLine('pskl.app.piskelController.isEmpty()'), true, 'Current piskel is considered as empty');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getWidth()'), 1, 'Piskel width is 1 pixel');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getHeight()'), 1, 'Piskel height is 1 pixel');
// Open export panel.
test.assertDoesntExist('.settings-section-import', 'Check if import panel is closed');
casper.click('[data-setting="import"]');
casper.waitForSelector('.settings-section-import', onImportPanelReady, test.timeout, 10000);
}
function onImportPanelReady() {
test.assert(isDrawerExpanded(), 'settings drawer is expanded');
test.assertExists('.settings-section-import', 'Check if import panel is opened');
replaceFunction(test,
'pskl.utils.FileUtils.readImageFile',
function (file, callback) {
var image = new Image();
image.onload = callback.bind(null, image);
// Source for a simple base64 encoded PNG, 2x2, with 2 different colors and 2 transparent pixels.
image.src = [
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0',
'kAAAAF0lEQVQYVwXBAQEAAACCIPw/uiAYi406Ig4EARK1RMAAAAAASUVORK5CYII='
].join('');
}
);
casper.echo('Clicking on Browse Images button');
test.assertExists('.file-input-button', 'The import image button is available');
// We can't really control the file picker from the test so we directly fire the event
casper.evaluate(
'function () {\
$.publish(Events.DIALOG_SHOW, {\
dialogId : "import",\
initArgs : {\
rawFiles: [{type: "image", name: "test-name.png"}]\
}\
});\
}'
);
casper.echo('Wait for .import-image-container');
casper.waitForSelector('.current-step.import-image-container', onImageImportReady, test.timeout, 10000);
}
function onImageImportReady() {
casper.echo('Found import-image-container');
// Click on export again to close the settings drawer.
test.assertEquals(getTextContent('.import-next-button'), 'import',
'Next button found, with text content \'import\'');
casper.click('.current-step .import-next-button');
// Since the current sprite is empty clicking on the button should directly finalize the import.
casper.waitForSelector('#dialog-container-wrapper:not(.show)', onPopupClosed, test.timeout, 10000);
}
function onPopupClosed() {
casper.echo('Import popup is closed, check the imported piskel content');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getWidth()'), 2, 'Piskel width is 2 pixels');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getHeight()'), 2, 'Piskel height is 2 pixels');
test.assertEquals(evalLine('pskl.app.piskelController.getLayers().length'), 1, 'Piskel has 1 layer');
test.assertEquals(evalLine('pskl.app.piskelController.getFrameCount()'), 1, 'Piskel has 1 frame');
}
casper
.start(casper.cli.get('baseUrl')+"/?debug&integration-test")
.then(function () {
casper.echo("URL loaded");
casper.waitForSelector('#drawing-canvas-container canvas', onTestStart, test.timeout, 20000);
})
.run(function () {
test.done();
});
});

View File

@ -1,7 +1,7 @@
/* globals casper, setPiskelFromGrid, isDrawerExpanded, getValue, isChecked,
evalLine, waitForEvent, replaceFunction, setPiskelFromImageSrc */
casper.test.begin('Double Image import test', 25, function(test) {
casper.test.begin('Double Image import test', 26, function(test) {
test.timeout = test.fail.bind(test, ['Test timed out']);
// Helper to retrieve the text content of the provided selector
@ -54,18 +54,21 @@ casper.test.begin('Double Image import test', 25, function(test) {
test.assert(!isDrawerExpanded(), 'settings drawer is closed');
waitForEvent('PISKEL_RESET', onPiskelReset, test.timeout);
// 1x1 black pixel
var src = [
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcS',
'JAAAADUlEQVQYV2NgYGD4DwABBAEAcCBlCwAAAABJRU5ErkJggg=='
].join('');
setPiskelFromImageSrc(src);
// For this test the most important is that the color service picked up the color from the sprite
// since it drives which flow will be used for the import.
casper.waitForSelector('.palettes-list-color:nth-child(1)', onPiskelPaletteUpdated, test.timeout, 10000);
}
function onPiskelReset() {
function onPiskelPaletteUpdated() {
// Check the expected piskel was correctly loaded.
test.assertEquals(evalLine('pskl.app.currentColorsService.getCurrentColors().length'), 1, 'Has 1 color');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getWidth()'), 1, 'Piskel width is 1 pixel');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getHeight()'), 1, 'Piskel height is 1 pixel');

View File

@ -1,7 +1,7 @@
/* globals casper, setPiskelFromGrid, isDrawerExpanded, getValue, isChecked,
evalLine, waitForEvent, replaceFunction, setPiskelFromImageSrc */
casper.test.begin('Simple Image import test', 26, function(test) {
casper.test.begin('Simple Image import test', 27, function(test) {
test.timeout = test.fail.bind(test, ['Test timed out']);
// Helper to retrieve the text content of the provided selector
@ -30,18 +30,21 @@ casper.test.begin('Simple Image import test', 26, function(test) {
test.assert(!isDrawerExpanded(), 'settings drawer is closed');
waitForEvent('PISKEL_RESET', onPiskelReset, test.timeout);
// 1x1 black pixel
var src = [
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcS',
'JAAAADUlEQVQYV2NgYGD4DwABBAEAcCBlCwAAAABJRU5ErkJggg=='
].join('');
setPiskelFromImageSrc(src);
// For this test the most important is that the color service picked up the color from the sprite
// since it drives which flow will be used for the import.
casper.waitForSelector('.palettes-list-color:nth-child(1)', onPiskelPaletteUpdated, test.timeout, 10000);
}
function onPiskelReset() {
function onPiskelPaletteUpdated() {
// Check the expected piskel was correctly loaded.
test.assertEquals(evalLine('pskl.app.currentColorsService.getCurrentColors().length'), 1, 'Has 1 color');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getWidth()'), 1, 'Piskel width is 1 pixel');
test.assertEquals(evalLine('pskl.app.piskelController.getPiskel().getHeight()'), 1, 'Piskel height is 1 pixel');

View File

@ -8,10 +8,11 @@
"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",
"pen.mirror.pensize.json",
"pen.secondary.color.json",
"selection.rectangular.json",
"squares.circles.json",

View File

@ -10,6 +10,7 @@
"layers.top.bottom.json",
"move.json",
"move-alllayers-allframes.json",
"pen.mirror.pensize.json",
"pen.secondary.color.json",
"selection.rectangular.json",
"squares.circles.json",

View File

@ -0,0 +1,125 @@
{
"events": [
{
"type": "pensize-event",
"penSize": 2
},
{
"type": "tool-event",
"toolId": "tool-vertical-mirror-pen"
},
{
"event": {
"type": "mousedown",
"button": 0,
"shiftKey": false,
"altKey": false,
"ctrlKey": false
},
"coords": {
"x": 1,
"y": 1
},
"type": "mouse-event"
},
{
"event": {
"type": "mousemove",
"button": 0,
"shiftKey": false,
"altKey": false,
"ctrlKey": false
},
"coords": {
"x": 1,
"y": 1
},
"type": "mouse-event"
},
{
"event": {
"type": "mousemove",
"button": 0,
"shiftKey": false,
"altKey": false,
"ctrlKey": false
},
"coords": {
"x": 1,
"y": 2
},
"type": "mouse-event"
},
{
"event": {
"type": "mousemove",
"button": 0,
"shiftKey": false,
"altKey": false,
"ctrlKey": false
},
"coords": {
"x": 1,
"y": 3
},
"type": "mouse-event"
},
{
"event": {
"type": "mousemove",
"button": 0,
"shiftKey": false,
"altKey": false,
"ctrlKey": false
},
"coords": {
"x": 1,
"y": 4
},
"type": "mouse-event"
},
{
"event": {
"type": "mousemove",
"button": 0,
"shiftKey": false,
"altKey": false,
"ctrlKey": false
},
"coords": {
"x": 1,
"y": 5
},
"type": "mouse-event"
},
{
"event": {
"type": "mouseup",
"button": 0,
"shiftKey": false,
"altKey": false,
"ctrlKey": false
},
"coords": {
"x": 1,
"y": 5
},
"type": "mouse-event"
},
{
"type": "pensize-event",
"penSize": 1
}
],
"initialState": {
"size": {
"width": 6,
"height": 6
},
"primaryColor": "#000000",
"secondaryColor": "rgba(0, 0, 0, 0)",
"selectedTool": "tool-pen",
"penSize": 1
},
"png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAGElEQVQYV2NkYGD4zwABjFAazAdxBkwCAIsLDAFt5z4tAAAAAElFTkSuQmCC"
}

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