Reviewed and improved export feature

This commit is contained in:
Giulio Cesare Solaroli 2015-05-07 16:23:57 +02:00
parent b812ea4efb
commit e329f6926a
18 changed files with 1195 additions and 474 deletions

View File

@ -469,6 +469,21 @@ div.overlay {
-ms-animation-delay: -0.0833s;
-o-animation-delay: -0.0833s;
animation-delay: -0.0833s; }
div.overlay .progressBar {
width: 100%;
background-color: #222;
height: 4px;
margin-top: 86px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px; }
div.overlay .progressBar .progress {
background-color: #999;
height: 4px;
display: block;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px; }
@-webkit-keyframes overlay-spin {
from {
@ -851,7 +866,8 @@ html {
-moz-flex: auto;
-ms-flex: auto;
flex: auto;
overflow: auto; }
overflow: scroll;
-webkit-overflow-scrolling: touch; }
#extraFeaturesPanel .extraFeatureIndex footer {
-webkit-box-flex: none;
-webkit-flex: none;
@ -869,6 +885,12 @@ html {
width: 100%;
height: 100%;
background-color: black; }
#extraFeaturesPanel .extraFeatureContent .extraFeature {
height: 100%; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .content {
height: 100%;
overflow: scroll;
-webkit-overflow-scrolling: touch; }
.container {
height: 100%;
@ -2015,6 +2037,8 @@ span.count {
background-color: #333; }
#extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li > div {
padding: 4px; }
#extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li.offlineCopy {
cursor: default; }
#extraFeaturesPanel .extraFeatureIndex > div ul li h2 {
font-weight: 300;
font-size: 14pt; }
@ -2091,9 +2115,9 @@ span.count {
#extraFeaturesPanel .extraFeatureContent .extraFeature h1 {
font-size: 20pt;
padding-bottom: 20px; }
#extraFeaturesPanel .extraFeatureContent form label {
#extraFeaturesPanel .extraFeatureContent .extraFeature form label {
display: none; }
#extraFeaturesPanel .extraFeatureContent form input {
#extraFeaturesPanel .extraFeatureContent .extraFeature form input {
display: block;
font-size: 18pt;
margin-bottom: 8px;
@ -2101,10 +2125,10 @@ span.count {
border: 0px solid white;
width: 350px;
color: black; }
#extraFeaturesPanel .extraFeatureContent form input.invalid {
#extraFeaturesPanel .extraFeatureContent .extraFeature form input.invalid {
border: 0px solid #ff9900;
color: gray; }
#extraFeaturesPanel .extraFeatureContent form p {
#extraFeaturesPanel .extraFeatureContent .extraFeature form p {
display: -webkit-box;
display: -webkit-flex;
display: -moz-flex;
@ -2116,7 +2140,7 @@ span.count {
-moz-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row; }
#extraFeaturesPanel .extraFeatureContent form p input {
#extraFeaturesPanel .extraFeatureContent .extraFeature form p input {
width: 30px;
-webkit-box-flex: auto;
-webkit-flex: auto;
@ -2124,7 +2148,7 @@ span.count {
-moz-flex: auto;
-ms-flex: auto;
flex: auto; }
#extraFeaturesPanel .extraFeatureContent form p span {
#extraFeaturesPanel .extraFeatureContent .extraFeature form p span {
-webkit-box-flex: auto;
-webkit-flex: auto;
-moz-box-flex: auto;
@ -2132,7 +2156,7 @@ span.count {
-ms-flex: auto;
flex: auto;
font-size: 12pt; }
#extraFeaturesPanel .extraFeatureContent form button {
#extraFeaturesPanel .extraFeatureContent .extraFeature form button {
font-family: "clipperz-font";
color: white;
font-size: 14pt;
@ -2146,10 +2170,33 @@ span.count {
-o-transition: background-color font-weight 0.2s linear;
-ms-transition: background-color font-weight 0.2s linear;
transition: background-color font-weight 0.2s linear; }
#extraFeaturesPanel .extraFeatureContent form button:disabled {
#extraFeaturesPanel .extraFeatureContent .extraFeature form button:disabled {
font-weight: 100;
background-color: #c0c0c0;
cursor: default; }
#extraFeaturesPanel .extraFeatureContent .extraFeature ul {
color: white; }
#extraFeaturesPanel .extraFeatureContent .extraFeature ul li {
padding-bottom: 40px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature h3 {
font-size: 18pt; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .description {
max-width: 500px;
padding: 10px 0px 20px 0px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .description p {
font-size: 10pt;
margin-bottom: 7px;
line-height: 1.4em;
color: #bbb; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .description p em {
text-decoration: underline; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .button {
display: inline;
color: white;
background-color: #ff9900;
font-size: 14pt;
border: 1px solid white;
padding: 6px 10px; }
.mainPage.narrow #extraFeaturesPanel .extraFeatureContent header {
display: block;

View File

@ -85,6 +85,7 @@ Clipperz_normalizedNewLine = '\x0d\x0a';
</div>
<span class="icon done" style="display:none">done</span>
<span class="icon failed" style="display:none">failed</span>
<span class="progressBar" style="display:none"><span class="progress"></span></span>
<span class="title">loading</span>
<div class="mask hidden"></div>
</div>

View File

@ -332,12 +332,13 @@ console.log("Record.Version.hasPendingChanges");
});
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.addCallback(function(listIn) {
return listIn.reduce(function(result, field) {
// return listIn.reduce(function(result, field) {
return MochiKit.Iter.reduce(function(result, field) {
var ref = field.reference;
result[ref] = field;
delete result[ref].reference;
return result;
}, {});
}, listIn, {});
});
deferredResult.callback();

View File

@ -45,9 +45,7 @@ Clipperz.PM.DataModel.Record = function(args) {
this._createNewDirectLoginFunction = args.createNewDirectLoginFunction || null;
this._tags = [];
this._directLogins = {};
this._versions = {};
this._currentRecordVersion = null;
@ -163,34 +161,20 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
//............................................................................
'tagRegExp': function () {
return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
'extractLabelFromFullLabel': function (aValue) {
return Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aValue);
},
'trimSpacesRegExp': function () {
return new RegExp('^\\s+|\\s+$', 'g');
'extractTagsFromFullLabel': function (aLabel) {
return Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aLabel);
},
// 'tagCleanupRegExp': function () {
// return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagSpace, 'g');
// },
//............................................................................
'filterOutTags': function (aValue) {
var value;
value = aValue;
value = value.replace(this.tagRegExp(), '');
value = value.replace(this.trimSpacesRegExp(), '');
return value;
},
'label': function () {
return Clipperz.Async.callbacks("Record.label", [
MochiKit.Base.method(this, 'fullLabel'),
MochiKit.Base.method(this, 'filterOutTags')
MochiKit.Base.method(this, 'extractLabelFromFullLabel')
], {trace:false});
},
@ -211,22 +195,6 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
//.........................................................................
'extractTagsFromFullLabel': function (aLabel) {
var tagRegEx;
var result;
var match;
result = {};
tagRegEx = this.tagRegExp();
match = tagRegEx.exec(aLabel);
while (match != null) {
result[match[1]] = true;
match = tagRegEx.exec(aLabel);
}
return result;
},
'tags': function () {
return Clipperz.Async.callbacks("Record.label", [
MochiKit.Base.method(this, 'fullLabel'),
@ -1173,10 +1141,9 @@ console.log("Record.hasPendingChanges RESULT", result);
'exportDirectLogins': function() {
var result;
var directLoginsObject = this.directLogins();
if (Object.keys(directLoginsObject).length == 0) {
if (MochiKit.Base.keys(directLoginsObject).length == 0) {
result = {};
} else {
var callbackObject = Object.keys(directLoginsObject).reduce(function(previous, current) {
@ -1209,10 +1176,10 @@ console.log("Record.hasPendingChanges RESULT", result);
deferredResult.addMethod(this, 'exportDirectLogins');
deferredResult.addCallback(function(directLoginsIn) { data['directLogins'] = directLoginsIn; });
deferredResult.addCallback(function() { return currentVersionObject.getKey(); }),
deferredResult.addMethod(this,function(keyIn) { data['currentVersionKey'] = keyIn; });
// deferredResult.addMethod(this,function(keyIn) { data['currentVersionKey'] = keyIn; });
deferredResult.addMethod(this, 'notes');
deferredResult.addMethod(this, function(notesIn) { data['notes'] = notesIn; });
deferredResult.addMethod(this,function() { currentVersion['reference'] = this.currentVersionReference(); });
// deferredResult.addMethod(this, function() { currentVersion['reference'] = this.currentVersionReference(); });
deferredResult.addCallback(function() { return currentVersionObject.exportFields(); }),
deferredResult.addCallback(function(fieldsIn) { currentVersion['fields'] = fieldsIn; });
deferredResult.addMethod(this, function() {
@ -1267,3 +1234,34 @@ Clipperz.PM.DataModel.Record.isRegularTag = function (aTag) {
Clipperz.PM.DataModel.Record.regExpForSearch = function (aSearch) {
return new RegExp(aSearch.replace(/[^A-Za-z0-9]/g, '\\$&'), 'i');
};
Clipperz.PM.DataModel.Record.tagRegExp = new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
Clipperz.PM.DataModel.Record.trimSpacesRegExp = new RegExp('^\\s+|\\s+$', 'g');
Clipperz.PM.DataModel.Record.extractLabelFromFullLabel = function (aValue) {
var value;
value = aValue;
value = value.replace(Clipperz.PM.DataModel.Record.tagRegExp, '');
value = value.replace(Clipperz.PM.DataModel.Record.trimSpacesRegExp, '');
return value;
};
Clipperz.PM.DataModel.Record.extractTagsFromFullLabel = function (aLabel) {
var tagRegEx;
var result;
var match;
result = {};
tagRegEx = Clipperz.PM.DataModel.Record.tagRegExp;
match = tagRegEx.exec(aLabel);
while (match != null) {
result[match[1]] = true;
match = tagRegEx.exec(aLabel);
}
return result;
};

View File

@ -30,18 +30,77 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass = React.createClass({
// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
},
/*
jsonExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'json');
},
htmlExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'html');
},
*/
isFeatureEnabled: function (aValue) {
return (this.props['features'].indexOf(aValue) > -1);
},
handleDownloadOfflineCopyLink: function (anEvent) {
if (this.isFeatureEnabled('OFFLINE_COPY')) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadOfflineCopy');
}
},
handleExportLink: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadExport');
},
//=========================================================================
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.h1({}, "Export"),
React.DOM.p({'className': 'link', 'onClick': MochiKit.Base.method(this, function(){
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export','json');
})}, "JSON"),
React.DOM.p({'className': 'link', 'onClick': MochiKit.Base.method(this, function(){
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export','printable');
})}, "Printable version")
React.DOM.div({'className': 'content'}, [
React.DOM.ul({}, [
React.DOM.li({}, [
React.DOM.h3({}, "Offline copy"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a read-only portable version of Clipperz. Very convenient when no Internet connection is available."),
React.DOM.p({}, "An offline copy is just a single HTML file that contains both the whole Clipperz web application and your encrypted data."),
React.DOM.p({}, "It is as secure as the hosted Clipperz service since they both share the same code and security architecture.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleDownloadOfflineCopyLink}, "download offline copy")
]),
React.DOM.li({}, [
React.DOM.h3({}, "HTML + JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a printer-friendly HTML file that lists the content of all your cards."),
React.DOM.p({}, "This same file also contains all your data in JSON format."),
React.DOM.p({}, "Beware: all data are unencrypted! Therefore make sure to properly store and manage this file.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleExportLink}, "download HTML+JSON")
]),
/*
React.DOM.li({}, [
React.DOM.h3({}, "Printing"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Click on the button below to open a new window displaying all your cards in a printable format."),
React.DOM.p({}, "If you are going to print for backup purposes, please consider the safer option provided by the “offline copy”.")
]),
React.DOM.a({'className':'button', 'onClick':this.htmlExport}, "HTML")
]),
React.DOM.li({}, [
React.DOM.h3({}, "Exporting to JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "JSON enables a “lossless” export of your cards. All the information will be preserved, including direct login configurations."),
React.DOM.p({}, "This custom format its quite convenient if you need to move some of all of your cards to a different Clipperz account. Or if you want to restore a card that has been accidentally deleted."),
React.DOM.p({}, "Click on the button below to start the export process.")
]),
React.DOM.a({'className':'button', 'onClick':this.jsonExport}, "JSON"),
])
*/
])
])
]);
},

View File

@ -78,6 +78,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
return React.DOM.div({className:'extraFeature deleteAccount'}, [
React.DOM.h1({}, "Delete Account"),
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
@ -92,6 +93,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
//~ React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error)
])
])
]);
},

View File

@ -36,7 +36,9 @@ Clipperz.PM.UI.Components.ExtraFeatures.DevicePINClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.h1({}, "Device PIN"),
React.DOM.div({'className': 'content'}, [
React.DOM.h3({}, this.props['PIN'])
])
]);
},

View File

@ -101,6 +101,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature passphrase'}, [
React.DOM.h1({}, "Change Passphrase"),
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
@ -121,7 +122,8 @@ Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
]),
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"),
]),
])
])
]);
},

View File

@ -53,11 +53,15 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
//-------------------------------------------------------------------------
'show': function (aMessage, showMask) {
'show': function (aMessage, showMask, showProgress) {
if (showMask === true) {
this.showMask();
}
if (showProgress === true) {
this.showProgressBar();
}
this.resetStatus();
this.setMessage(aMessage);
MochiKit.DOM.removeElementClass(this.element(), 'ios-overlay-hide');
@ -66,6 +70,7 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
'done': function (aMessage, aDelayBeforeHiding) {
this.hideMask();
this.hideProgressBar();
this.completed(this.showDoneIcon, aMessage, aDelayBeforeHiding);
},
@ -114,6 +119,7 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
'hide': function () {
var element = this.element();
this.hideProgressBar();
MochiKit.DOM.removeElementClass(element, 'ios-overlay-show');
MochiKit.DOM.addElementClass(element, 'ios-overlay-hide');
return MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
@ -133,6 +139,21 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
//-------------------------------------------------------------------------
'showProgressBar': function () {
MochiKit.Style.showElement(this.getElement('progressBar'));
},
'hideProgressBar': function () {
MochiKit.Style.hideElement(this.getElement('progressBar'));
},
'updateProgress': function (aProgressPercentage) {
MochiKit.Style.setElementDimensions(this.getElement('progress'), {'w': aProgressPercentage}, '%');
//console.log("OVERLAY - updating progress: " + aProgressPercentage + "%");
},
//-------------------------------------------------------------------------
'defaultDelay': function () {
return this._defaultDelay;
},

View File

@ -70,9 +70,13 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
//=========================================================================
showExtraFeatureComponent: function (aComponentName) {
toggleExtraFeatureComponent: function (aComponentName) {
return MochiKit.Base.bind(function () {
if (this.state['extraFeatureComponentName'] != aComponentName) {
this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName);
} else {
this.hideExtraFeatureContent();
}
}, this);
},
@ -118,7 +122,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.li({'key':'account', 'className':this.state['index']['account'] ? 'open' : 'closed'}, [
React.DOM.h1({'key':'accountH1', 'onClick':this.toggleIndexState('account')}, "Account"),
React.DOM.ul({'key':'accountUL'}, [
React.DOM.li({'key':'account_1', 'onClick':this.showExtraFeatureComponent('Passphrase'), 'className':(this.state['extraFeatureComponentName'] == 'Passphrase') ? 'selected' : ''}, [
React.DOM.li({'key':'account_1', 'onClick':this.toggleExtraFeatureComponent('Passphrase'), 'className':(this.state['extraFeatureComponentName'] == 'Passphrase') ? 'selected' : ''}, [
React.DOM.h2({'key':'account_1_h2'}, "Passphrase"),
React.DOM.div({'key':'account_1_div'}, [
React.DOM.p({'key':'account_1_p'}, "Change your account passphrase.")
@ -130,7 +134,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.p({}, "")
])
]),
React.DOM.li({'key':'account_3', 'onClick':this.showExtraFeatureComponent('DevicePIN')}, [
React.DOM.li({'key':'account_3', 'onClick':this.toggleExtraFeatureComponent('DevicePIN')}, [
React.DOM.h2({}, "Device PIN"),
React.DOM.div({}, [
React.DOM.p({}, "Configure a PIN that will allow to get access to your cards, but only on this device.")
@ -142,7 +146,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.p({}, "")
])
]),
React.DOM.li({'key':'account_5', 'onClick':this.showExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
React.DOM.li({'key':'account_5', 'onClick':this.toggleExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
React.DOM.h2({}, "Delete account"),
React.DOM.div({}, [
React.DOM.p({}, "Delete your account for good.")
@ -182,29 +186,29 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.li({'key':'data', 'className':this.state['index']['data'] ? 'open' : 'closed'}, [
React.DOM.h1({'onClick':this.toggleIndexState('data')}, "Data"),
React.DOM.ul({'key':'data'}, [
React.DOM.li({'key':'data_1'}, [
React.DOM.h2({}, "Offline copy"),
React.DOM.div({}, [
React.DOM.p({}, "With just one click you can dump all your encrypted data from Clipperz servers to your hard disk and create a read-only offline version of Clipperz to be used when you are not connected to the Internet."),
React.DOM.a({'className':Clipperz.PM.UI.Components.classNames(offlineCopyButtonClasses), 'onClick':this.handleDownloadOfflineCopyLink}, "Download")
])
]),
// React.DOM.li({'key':'data_1', 'className':'offlineCopy'}, [
// React.DOM.h2({}, "Offline copy"),
// React.DOM.div({}, [
// React.DOM.p({}, "With just one click you can dump all your encrypted data from Clipperz servers to your hard disk and create a read-only offline version of Clipperz to be used when you are not connected to the Internet."),
// React.DOM.a({'className':Clipperz.PM.UI.Components.classNames(offlineCopyButtonClasses), 'onClick':this.handleDownloadOfflineCopyLink}, "Download")
// ])
// ]),
React.DOM.li({'key':'data_2'}, [
React.DOM.h2({}, "Import"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "CSV, JSON, …")
])
]),
React.DOM.li({'key':'data_3', 'onClick':this.showExtraFeatureComponent('DataExport')}, [
React.DOM.li({'key':'data_3', 'onClick':this.toggleExtraFeatureComponent('DataExport'), 'className':(this.state['extraFeatureComponentName'] == 'DataExport') ? 'selected' : ''}, [
React.DOM.h2({}, "Export"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "Offline copy, printable version, JSON, …")
])
]),
React.DOM.li({'key':'data_4'}, [
React.DOM.h2({}, "Sharing"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "Securely share cards with other users")
])
])
])
@ -229,17 +233,20 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
render: function () {
//console.log("ExtraFeaturesPanel props", this.props);
var isOpen = (this.props['settingsPanelStatus'] == 'OPEN');
var isFullyOpen = isOpen && this.state['isFullyOpen'];
var classes = {
'panel': true,
'right': true,
'open': this.props['settingsPanelStatus'] == 'OPEN',
'fullOpen': this.state['isFullyOpen']
'open': isOpen,
'fullOpen': isFullyOpen
}
return React.DOM.div({'key':'extraFeaturesPanel', 'id':'extraFeaturesPanel', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [
this.renderIndex(),
this.renderContent(),
// (this.props['settingsPanelStatus'] == 'OPEN') ? this.renderContent() : null,
]);
}

View File

@ -21,86 +21,110 @@ refer to http://www.clipperz.com.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI');
Clipperz.PM.UI.ExportController = function(args) {
this._type = args['type'] || Clipperz.Base.exception.raise('MandatoryParameter');
this._recordsInfo = args['recordsInfo'] || Clipperz.Base.exception.raise('MandatoryParameter');
this._target = Clipperz.PM.Crypto.randomKey();
// https://github.com/eligrey/FileSaver.js
// https://github.com/eligrey/Blob.js
this._style = "body {"+
" margin: 0;"+
" padding: 0;"+
" font-family: monospace;"+
Clipperz.PM.UI.ExportController = function(args) {
this._recordsInfo = args['recordsInfo'] || Clipperz.Base.exception.raise('MandatoryParameter');
this._processedRecords = 0;
this._style =
"body {" +
"font-family: 'Dejavu Sans', monospace;" +
"margin: 0px;" +
"}" +
""+
"p {"+
" padding-left: 1em;"+
"header {" +
"padding: 10px;" +
"border-bottom: 2px solid black;" +
"}" +
""+
"h1 {" +
" color: #ff9900;"+
" background: black;"+
" box-shadow: 0px 5px 6px 0 rgba(0, 0, 0, 0.15);"+
" margin: 0;"+
" padding:1em;"+
"margin: 0px;" +
"}" +
""+
".progressBar {"+
" position: absolute;"+
" width: 100%;"+
" margin-top: 0px;"+
" "+
"h2 {" +
"margin: 0px;" +
"padding-top: 10px;" +
"}" +
""+
"#completed {"+
" background: #ff9900;"+
"h3 {" +
"margin: 0px;" +
"}" +
"h5 {" +
"margin: 0px;" +
"color: gray;" +
"}" +
"ul {" +
"margin: 0px;" +
"padding: 0px;" +
"}" +
"div > ul > li {" +
"border-bottom: 1px solid black;" +
"padding: 10px;" +
"}" +
"div > ul > li.archived {" +
"background-color: #ddd;" +
"}" +
"ul > li > ul > li {" +
"font-size: 9pt;" +
"display: inline-block;" +
"}" +
"ul > li > ul > li:after {" +
"content: \",\";" +
"padding-right: 5px;" +
"}" +
"ul > li > ul > li:last-child:after {" +
"content: \"\";" +
"padding-right: 0px;" +
"}" +
"dl {" +
"}" +
"dt {" +
"color: gray;" +
"font-size: 9pt;" +
"}" +
"dd {" +
"margin: 0px;" +
"margin-bottom: 5px;" +
"padding-left: 10px;" +
"}" +
"div > div {" +
"background-color: black;" +
"color: white;" +
" width: 0;"+
" overflow: hidden;"+
" font-size: 0.8em;"+
" box-shadow: 0px 4px 6px 0 rgba(0, 0, 0, 0.15);"+
"padding: 10px;" +
"}" +
""+
"#printableUl {"+
"textarea {" +
"width: 100%;" +
" height:80%;"+
" margin: 0;"+
" padding: 0;"+
" list-style-type: none;"+
"height: 200px;" +
"}" +
""+
"#printableUl li {"+
" border: 1px solid #1863a1;"+
" margin: 1em;"+
""+
"@media print {" +
"div > div, header > div {" +
"display: none !important;" +
"}" +
""+
"#printableUl li .label {"+
" background: #1863a1;"+
" color: white;"+
" display: block;"+
" padding: 1em;"+
"ul > li {" +
"page-break-inside: avoid;" +
"} " +
""+
"#printableUl li dl {"+
" padding: 1em;"+
"}"+
""+
"#printableUl li dl dt {"+
" color: darkgray;"+
"}"+
""+
"#printableUl li dl dd {"+
" padding: 0;"+
" margin: 0 0 .5em 0;"+
"}"+
""+
"#printableUl li .notes {"+
" font-style: italic;"+
" padding: 1em 0 0 1em;"+
" display: block;"+
"}" +
"";
return this;
@ -114,188 +138,141 @@ MochiKit.Base.update(Clipperz.PM.UI.ExportController.prototype, {
//-----------------------------------------------------------------------------
'type': function () {
return this._type;
},
'recordsInfo': function () {
return this._recordsInfo;
},
'target': function () {
return this._target;
//=============================================================================
'reportRecordExport': function (aRecordData) {
var percentage;
var exportedCardsCount;
var totalCardsToExport;
this._processedRecords = this._processedRecords + 1;
exportedCardsCount = this._processedRecords;
totalCardsToExport = this.recordsInfo().length;
percentage = Math.round(100 * exportedCardsCount / totalCardsToExport);
//console.log("PROCESSING " + exportedCardsCount + "/" + totalCardsToExport + " - " + percentage + "%");
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updateProgress', percentage);
return MochiKit.Async.succeed(aRecordData);
},
//=============================================================================
'setWindowTitle': function (aWindow, aTitle) {
aWindow.document.title = aTitle;
},
'renderCardToHtml': function (jsonCardData) {
var label = Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(jsonCardData.label);
var allTags = MochiKit.Base.keys(Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(jsonCardData.label));
var regularTags = MochiKit.Base.filter(Clipperz.PM.DataModel.Record.isRegularTag, allTags);
var isArchived = MochiKit.Iter.some(allTags, MochiKit.Base.partial(MochiKit.Base.objEqual, Clipperz.PM.DataModel.Record.archivedTag));
'setWindowBody': function (aWindow, anHTML) {
aWindow.document.body.innerHTML = anHTML;
},
//=============================================================================
'initialWindowSetup': function (aWindow) {
var dom = MochiKit.DOM.DIV({'id': 'main'},
MochiKit.DOM.H1("Clipperz Exported Data (loading...)"),
MochiKit.DOM.DIV({'class': 'progressBar'},
MochiKit.DOM.DIV({'id': 'completed'},
MochiKit.DOM.P({'style': 'margin:0; padding:0; text-align:center;'}, MochiKit.DOM.SPAN({'id': 'nCompleted'},"0"),"/",MochiKit.DOM.SPAN({'id': 'nTotal'},"") )
)
)
);
aWindow.document.getElementsByTagName('head')[0].appendChild( MochiKit.DOM.STYLE(this._style) );
this.setWindowTitle(aWindow, "Clipperz Exported Data (loading...)");
this.setWindowBody (aWindow, MochiKit.DOM.toHTML(dom));
},
//-----------------------------------------------------------------------------
'updateWindowWithHTMLContent': function (aWindow, anHtml) {
this.setWindowBody(aWindow, anHtml);
},
'updateWindowJSON': function (aWindow, exportedJSON) {
var dom = MochiKit.DOM.DIV({'id': 'main'},
MochiKit.DOM.H1("Clipperz Exported Data"),
MochiKit.DOM.P("You can now save the following data and load it at any time using the Clipperz import feature."),
MochiKit.DOM.TEXTAREA({'style': 'width:100%; height:80%'}, Clipperz.Base.serializeJSON(exportedJSON))
);
this.setWindowTitle(aWindow, "Clipperz Exported Data");
this.setWindowBody(aWindow, MochiKit.DOM.toHTML(dom));
},
'updateWindowPrintable': function (aWindow, exportedJSON) {
var dom = MochiKit.DOM.DIV({'id': 'main'},
MochiKit.DOM.H1("Clipperz Exported Data"),
MochiKit.DOM.P("You can now print this page and store it in a safe place."),
MochiKit.DOM.UL({'id': 'printableUl'},
exportedJSON.map(function(card){
var label = (card.label.indexOf('')>=0) ? card.label.slice(0,card.label.indexOf('')).trim() : card.label;
var notes = (card.data.notes) ? MochiKit.DOM.SPAN({'class': 'notes'}, card.data.notes) : "";
return MochiKit.DOM.LI({},
MochiKit.DOM.SPAN({'class': 'label'}, label),
notes,
return MochiKit.DOM.LI({'class': isArchived ? 'archived' : ""},
MochiKit.DOM.H2({}, label),
(regularTags.length > 0) ? MochiKit.DOM.UL({}, MochiKit.Base.map(function (tag) { return MochiKit.DOM.LI({}, tag);}, regularTags)): null,
MochiKit.DOM.DIV({},
MochiKit.DOM.DL({},
Object.keys(card.currentVersion.fields).map(function(key) {
MochiKit.Base.map(function(key) {
return [
MochiKit.DOM.DT(card.currentVersion.fields[key].label),
MochiKit.DOM.DD(card.currentVersion.fields[key].value),
MochiKit.DOM.DT(jsonCardData.currentVersion.fields[key].label),
MochiKit.DOM.DD(jsonCardData.currentVersion.fields[key].value),
];
})
}, MochiKit.Base.keys(jsonCardData.currentVersion.fields))
)
);
})
)
);
this.setWindowTitle(aWindow, "Clipperz Exported Data");
this.setWindowBody(aWindow, MochiKit.DOM.toHTML(dom));
},
'updateWindowError': function (aWindow, errorMessage) {
this.setWindowBody(aWindow,
"<h3>Error</h3>"+
"<p>The following error occured while exporting your data:</p>"+
"<code>"+errorMessage+"</code>"
),
jsonCardData.data.notes ? MochiKit.DOM.P({}, jsonCardData.data.notes) : null
);
},
//=============================================================================
'renderToHtml': function (jsonData) {
var title;
var style;
var date;
var body;
'runExportJSON': function (aWindow) {
var deferredResult;
var exportedRecords;
title = "Clipperz data";
style = this._style;
date = "dd/mm/yyyy";
var totalRecords = this.recordsInfo().length;
body = MochiKit.DOM.DIV({},
MochiKit.DOM.HEADER({},
MochiKit.DOM.H1({}, "Your data on Clipperz"),
MochiKit.DOM.H5({}, "Export date: " + date),
MochiKit.DOM.DIV({},
MochiKit.DOM.P({}, "Security warning - This file lists the content of all your cards in a printer-friendly format. At the very bottom, the same content is also available in JSON format."),
MochiKit.DOM.P({}, "Beware: all data are unencrypted! Therefore make sure to properly store and manage this file. We recommend to delete it as soon as it is no longer needed."),
MochiKit.DOM.P({}, "If you are going to print its content on paper, store the printout in a safe and private place!"),
MochiKit.DOM.P({}, "And, if you need to access your data when no Internet connection is available, please consider the much safer option of creating an offline copy.")
)
),
exportedRecords = 0;
MochiKit.DOM.UL({}, MochiKit.Base.map(this.renderCardToHtml, jsonData)),
MochiKit.DOM.DIV({},
MochiKit.DOM.H3({}, "JSON content"),
MochiKit.DOM.DIV({},
MochiKit.DOM.P({}, "Instructions on how to use JSON content"),
MochiKit.DOM.P({}, "The JSON version of your data may be useful if you want to move the whole content of your Clipperz account to a new Clipperz account or recover a card that has been accidentally deleted. Just follow these instructions:"),
MochiKit.DOM.OL({},
MochiKit.DOM.LI({}, "Login to your Clipperz account and go to \"Data > Import\"."),
MochiKit.DOM.LI({}, "Select the JSON option."),
MochiKit.DOM.LI({}, "Copy and paste the JSON content in the form.")
),
MochiKit.DOM.P({}, "Of course, the unencrypted JSON content won't be transmitted to the Clipperz server.")
),
MochiKit.DOM.TEXTAREA({}, Clipperz.Base.serializeJSON(jsonData)),
MochiKit.DOM.FOOTER({},
MochiKit.DOM.P({},
"This file has been downloaded from clipperz.is, a service by Clipperz Srl. - ",
MochiKit.DOM.A({'href':'https://clipperz.is/terms_service/'}, "Terms of service"),
" - ",
MochiKit.DOM.A({'href':'https://clipperz.is/privacy_policy/'}, "Privacy policy")
),
MochiKit.DOM.H4({}, "Clipperz - keep it to yourself")
)
)
);
deferredResult = new Clipperz.Async.Deferred("DirectLoginRunner.exportJSON", {trace:false});
deferredResult.addMethod(this, 'initialWindowSetup', aWindow);
deferredResult.addCallback(function() { return "Export Data"});
deferredResult.addMethod(this, 'setWindowTitle', aWindow);
return '<html><head><title>' + title + '</title><style type="text/css">' + style + '</style></head><body>' + MochiKit.DOM.toHTML(body) + '</body></html>';
},
deferredResult.addMethod( this, function() { return this.recordsInfo(); });
deferredResult.addCallback( MochiKit.Base.map, function(recordIn) {
var dr = new Clipperz.Async.Deferred("DirectLoginRunner.exportJSON__exportRecord", {trace:false});
dr.addMethod(recordIn._rowObject, 'export');
dr.addCallback(MochiKit.Base.method(this, function (exportedRecord) {
var percentage = Math.round(100*exportedRecords/totalRecords);
//----------------------------------------------------------------------------
aWindow.document.getElementById('nCompleted').innerText = ++exportedRecords;
aWindow.document.getElementById('nTotal').innerText = totalRecords;
aWindow.document.getElementById('completed').style.width = percentage+'%';
'saveResult': function (exportedJSON) {
var blob;
var sortedJSON;
return exportedRecord;
}));
dr.callback();
return dr;
});
sortedJSON = MochiKit.Iter.sorted(exportedJSON, function(a,b) { return a.label.toUpperCase().localeCompare(b.label.toUpperCase()); } );
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.addMethod( this, function(exportedJSONIn) {
// console.log('return',exportedJSONIn);
sortedJSON = exportedJSONIn.sort( function(a,b) { return a.label.toUpperCase().localeCompare(b.label.toUpperCase()); } );
switch (this.type()) {
case 'json':
this.updateWindowJSON(aWindow,exportedJSONIn);
break;
case 'printable':
this.updateWindowPrintable(aWindow,exportedJSONIn);
break;
default:
this.updateWindowError(aWindow,"ExportController.runExportJSON: invalid value '"+this.type()+"' for parameter 'type'.");
}
});
deferredResult.callback();
return deferredResult;
blob = new Blob([this.renderToHtml(sortedJSON)], {type: "text/html;charset=utf-8"});
saveAs(blob, "clipperz_data.html");
},
//=============================================================================
'run': function () {
var newWindow;
var deferredResult;
var self = this;
newWindow = window.open("", this.target());
deferredResult = new Clipperz.Async.Deferred("ExportController.run", {trace:false});
deferredResult.addCallback(MochiKit.Base.map, function(recordIn) {
var innerDeferredResult;
return this.runExportJSON(newWindow);
},
innerDeferredResult = new Clipperz.Async.Deferred("ExportController.run__exportRecord", {trace:false});
innerDeferredResult.addMethod(recordIn._rowObject, 'export');
innerDeferredResult.addMethod(self, 'reportRecordExport');
innerDeferredResult.callback();
//=============================================================================
return innerDeferredResult;
});
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.addMethod(this, 'saveResult');
deferredResult.callback(this.recordsInfo());
'test': function () {
var iFrame;
var newWindow;
iFrame = MochiKit.DOM.createDOM('iframe');
MochiKit.DOM.appendChildNodes(MochiKit.DOM.currentDocument().body, iFrame);
newWindow = iFrame.contentWindow;
return this.runDirectLogin(newWindow);
return deferredResult;
},
//=============================================================================
__syntaxFix__: "syntax fix"
});
//-----------------------------------------------------------------------------
Clipperz.PM.UI.ExportController.exportJSON = function (recordsInfoIn, typeIn) {
var runner;
runner = new Clipperz.PM.UI.ExportController({type:typeIn, recordsInfo: recordsInfoIn});
return runner.run();
};

View File

@ -63,7 +63,9 @@ Clipperz.PM.UI.MainController = function() {
this.registerForNotificationCenterEvents([
'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
'changePassphrase', 'deleteAccount',
'export',
// 'export',
'downloadExport',
'updateProgress',
'toggleSelectionPanel', 'toggleSettingsPanel',
'matchMediaQuery', 'unmatchMediaQuery',
'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards',
@ -106,6 +108,10 @@ MochiKit.Base.update(Clipperz.PM.UI.MainController.prototype, {
return this._overlay;
},
updateProgress_handler: function (aProgressPercentage) {
this.overlay().updateProgress(aProgressPercentage);
},
loginForm: function () {
return this._loginForm;
},
@ -1240,8 +1246,24 @@ console.log("THE BROWSER IS OFFLINE");
//----------------------------------------------------------------------------
export_handler: function(exportType) {
return Clipperz.PM.UI.ExportController.exportJSON( this.recordsInfo(), exportType );
// export_handler: function(exportType) {
// return Clipperz.PM.UI.ExportController.exportJSON( this.recordsInfo(), exportType );
// },
downloadExport_handler: function () {
var exportController;
var deferredResult;
exportController = new Clipperz.PM.UI.ExportController({'recordsInfo': this.recordsInfo()});
deferredResult = new Clipperz.Async.Deferred("MainController.downloadExport_handler", {trace: false});
deferredResult.addMethod(this.overlay(), 'show', "exporting …", true, true);
// deferredResult.addCallback(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'toggleSettingsPanel');
deferredResult.addMethod(exportController, 'run');
deferredResult.addMethod(this.overlay(), 'done', "", 1);
deferredResult.callback();
return deferredResult;
},
//----------------------------------------------------------------------------

View File

@ -0,0 +1,234 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
/* Blob.js
* A Blob implementation.
* 2014-07-24
*
* By Eli Grey, http://eligrey.com
* By Devin Samarin, https://github.com/dsamarin
* License: X11/MIT
* See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
*/
/*global self, unescape */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
(function (view) {
"use strict";
view.URL = view.URL || view.webkitURL;
if (view.Blob && view.URL) {
try {
new Blob;
return;
} catch (e) {}
}
// Internally we use a BlobBuilder implementation to base Blob off of
// in order to support older browsers that only have BlobBuilder
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
var
get_class = function(object) {
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
}
, FakeBlobBuilder = function BlobBuilder() {
this.data = [];
}
, FakeBlob = function Blob(data, type, encoding) {
this.data = data;
this.size = data.length;
this.type = type;
this.encoding = encoding;
}
, FBB_proto = FakeBlobBuilder.prototype
, FB_proto = FakeBlob.prototype
, FileReaderSync = view.FileReaderSync
, FileException = function(type) {
this.code = this[this.name = type];
}
, file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" ")
, file_ex_code = file_ex_codes.length
, real_URL = view.URL || view.webkitURL || view
, real_create_object_URL = real_URL.createObjectURL
, real_revoke_object_URL = real_URL.revokeObjectURL
, URL = real_URL
, btoa = view.btoa
, atob = view.atob
, ArrayBuffer = view.ArrayBuffer
, Uint8Array = view.Uint8Array
, origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
}
// Polyfill URL
if (!real_URL.createObjectURL) {
URL = view.URL = function(uri) {
var
uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
, uri_origin
;
uri_info.href = uri;
if (!("origin" in uri_info)) {
if (uri_info.protocol.toLowerCase() === "data:") {
uri_info.origin = null;
} else {
uri_origin = uri.match(origin);
uri_info.origin = uri_origin && uri_origin[1];
}
}
return uri_info;
};
}
URL.createObjectURL = function(blob) {
var
type = blob.type
, data_URI_header
;
if (type === null) {
type = "application/octet-stream";
}
if (blob instanceof FakeBlob) {
data_URI_header = "data:" + type;
if (blob.encoding === "base64") {
return data_URI_header + ";base64," + blob.data;
} else if (blob.encoding === "URI") {
return data_URI_header + "," + decodeURIComponent(blob.data);
} if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
};
URL.revokeObjectURL = function(object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function(data/*, endings*/) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = ""
, buf = new Uint8Array(data)
, i = 0
, buf_len = buf.length
;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function(type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function() {
return "[object BlobBuilder]";
};
FB_proto.slice = function(start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length)
, type
, this.encoding
);
};
FB_proto.toString = function() {
return "[object Blob]";
};
FB_proto.close = function() {
this.size = 0;
delete this.data;
};
return FakeBlobBuilder;
}(view));
view.Blob = function(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
if (Uint8Array && blobParts[i] instanceof Uint8Array) {
builder.append(blobParts[i].buffer);
}
else {
builder.append(blobParts[i]);
}
}
}
var blob = builder.getBlob(type);
if (!blob.slice && blob.webkitSlice) {
blob.slice = blob.webkitSlice;
}
return blob;
};
var getPrototypeOf = Object.getPrototypeOf || function(object) {
return object.__proto__;
};
view.Blob.prototype = getPrototypeOf(new view.Blob());
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

View File

@ -0,0 +1,271 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 2015-03-04
*
* By Eli Grey, http://eligrey.com
* License: X11/MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs
// IE 10+ (native saveAs)
|| (typeof navigator !== "undefined" &&
navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
// Everyone else
|| (function(view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof navigator !== "undefined" &&
/MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = "download" in save_link
, click = function(node) {
var event = doc.createEvent("MouseEvents");
event.initMouseEvent(
"click", true, false, view, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
node.dispatchEvent(event);
}
, webkit_req_fs = view.webkitRequestFileSystem
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
, throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
, fs_min_size = 0
// See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
// https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
// for the reasoning behind the timeout and revocation flow
, arbitrary_revoke_timeout = 500 // in ms
, revoke = function(file) {
var revoker = function() {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
if (view.chrome) {
revoker();
} else {
setTimeout(revoker, arbitrary_revoke_timeout);
}
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, FileSaver = function(blob, name) {
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, blob_changed = false
, object_url
, target_view
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
// don't create more object URLs than needed
if (blob_changed || !object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (target_view) {
target_view.location.href = object_url;
} else {
var new_tab = view.open(object_url, "_blank");
if (new_tab == undefined && typeof safari !== "undefined") {
//Apple do not allow window.open, see http://bit.ly/1kZffRI
view.location.href = object_url
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
}
, abortable = function(func) {
return function() {
if (filesaver.readyState !== filesaver.DONE) {
return func.apply(this, arguments);
}
};
}
, create_if_not_found = {create: true, exclusive: false}
, slice
;
filesaver.readyState = filesaver.INIT;
if (!name) {
name = "download";
}
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
save_link.href = object_url;
save_link.download = name;
click(save_link);
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
return;
}
// prepend BOM for UTF-8 XML and text/plain types
if (/^\s*(?:text\/(?:plain|xml)|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
blob = new Blob(["\ufeff", blob], {type: blob.type});
}
// Object and web filesystem URLs have a problem saving in Google Chrome when
// viewed in a tab, so I force save with application/octet-stream
// http://code.google.com/p/chromium/issues/detail?id=91158
// Update: Google errantly closed 91158, I submitted it again:
// https://code.google.com/p/chromium/issues/detail?id=389642
if (view.chrome && type && type !== force_saveable_type) {
slice = blob.slice || blob.webkitSlice;
blob = slice.call(blob, 0, blob.size, force_saveable_type);
blob_changed = true;
}
// Since I can't be sure that the guessed media type will trigger a download
// in WebKit, I append .download to the filename.
// https://bugs.webkit.org/show_bug.cgi?id=65440
if (webkit_req_fs && name !== "download") {
name += ".download";
}
if (type === force_saveable_type || webkit_req_fs) {
target_view = view;
}
if (!req_fs) {
fs_error();
return;
}
fs_min_size += blob.size;
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
var save = function() {
dir.getFile(name, create_if_not_found, abortable(function(file) {
file.createWriter(abortable(function(writer) {
writer.onwriteend = function(event) {
target_view.location.href = file.toURL();
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "writeend", event);
revoke(file);
};
writer.onerror = function() {
var error = writer.error;
if (error.code !== error.ABORT_ERR) {
fs_error();
}
};
"writestart progress write abort".split(" ").forEach(function(event) {
writer["on" + event] = filesaver["on" + event];
});
writer.write(blob);
filesaver.abort = function() {
writer.abort();
filesaver.readyState = filesaver.DONE;
};
filesaver.readyState = filesaver.WRITING;
}), fs_error);
}), fs_error);
};
dir.getFile(name, {create: false}, abortable(function(file) {
// delete file if it already exists
file.remove();
save();
}), abortable(function(ex) {
if (ex.code === ex.NOT_FOUND_ERR) {
save();
} else {
fs_error();
}
}));
}), fs_error);
}), fs_error);
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name) {
return new FileSaver(blob, name);
}
;
FS_proto.abort = function() {
var filesaver = this;
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "abort");
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(
typeof self !== "undefined" && self
|| typeof window !== "undefined" && window
|| this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
define([], function() {
return saveAs;
});
}

View File

@ -58,6 +58,9 @@
"-- Modernizr/modernizr-2.8.2.js",
"OnMediaQuery/onmediaquery-0.2.0.js",
"FileSaver/Blob.js",
"FileSaver/FileSaver.js",
"-- IT WOULD BE NICE TO BE ABLE TO GET RID OF THESE IMPORTS",
"Clipperz/YUI/Utils.js",
"Clipperz/YUI/DomHelper.js",

View File

@ -209,9 +209,8 @@ html {
& > div {
@include flex(auto);
overflow: auto;
// overflow: auto;
@include overflow-scroll();
}
footer {
@ -226,6 +225,20 @@ html {
height: 100%;
// background-color: rgba( 0, 0, 0, 0.95);
background-color: black;
.extraFeature {
// @include flexbox();
height: 100%;
h1 {
// @include flex(none);
}
.content {
// @include flex(auto);
height: 100%;
@include overflow-scroll();
}
}
}
}

View File

@ -126,6 +126,23 @@ div.overlay {
div.bar11 {@include transform(300deg, 0, -142%); @include animation-delay(-0.16670s);}
div.bar12 {@include transform(330deg, 0, -142%); @include animation-delay(-0.08330s);}
}
.progressBar {
// display: block;
width: 100%;
background-color: #222;
height: 4px;
margin-top: 86px;
@include border-radius(2px);
.progress {
background-color: #999;
// width: 70%;
height: 4px;
display: block;
@include border-radius(2px);
}
}
}
//========================================================

View File

@ -86,6 +86,10 @@ refer to http://www.clipperz.com.
& > div {
padding: 4px;
}
&.offlineCopy {
cursor: default;
}
}
&.open {
@ -178,38 +182,6 @@ refer to http://www.clipperz.com.
font-size: 20pt;
padding-bottom: 20px;
}
}
/*
.changePassphraseForm {
label {
display: block;
}
input {
display: block;
}
}
.deleteAccountForm {
margin-top: 1em;
label {
display: block;
}
input {
display: inline-block;
margin-right: 1em;
margin-bottom: 1em;
}
.confirmCheckbox {
display: inline-block;
}
}
*/
form {
@ -251,13 +223,9 @@ refer to http://www.clipperz.com.
button {
font-family: "clipperz-font";
// min-height: 48px;
// min-width: 48px;
color: white;
// font-size: 24pt;
font-size: 14pt;
// font-weight: 500;
border: 0px;
margin-top: 20px;
@ -284,6 +252,82 @@ refer to http://www.clipperz.com.
// }
}
ul {
color: white;
li {
padding-bottom: 40px;
}
}
h3 {
font-size: 18pt;
}
.description {
max-width: 500px;
padding: 10px 0px 20px 0px;
p {
font-size: 10pt;
margin-bottom: 7px;
line-height: 1.4em;
color:#bbb;
em {
text-decoration: underline;
}
}
}
.button {
display: inline;
color: white;
background-color: $main-color;
font-size: 14pt;
border: 1px solid white;
padding: 6px 10px;
&:after {
};
}
}
/*
.changePassphraseForm {
label {
display: block;
}
input {
display: block;
}
}
.deleteAccountForm {
margin-top: 1em;
label {
display: block;
}
input {
display: inline-block;
margin-right: 1em;
margin-bottom: 1em;
}
.confirmCheckbox {
display: inline-block;
}
}
*/
}
}