From e329f6926a1d86943f5c44cc6c01942efcf8d9bd Mon Sep 17 00:00:00 2001 From: Giulio Cesare Solaroli Date: Thu, 7 May 2015 16:23:57 +0200 Subject: [PATCH] Reviewed and improved export feature --- frontend/delta/css/clipperz.css | 167 ++++--- frontend/delta/html/index_template.html | 1 + .../Clipperz/PM/DataModel/Record.Version.js | 5 +- .../delta/js/Clipperz/PM/DataModel/Record.js | 96 ++-- .../UI/Components/ExtraFeatures/DataExport.js | 71 ++- .../Components/ExtraFeatures/DeleteAccount.js | 26 +- .../UI/Components/ExtraFeatures/DevicePIN.js | 4 +- .../UI/Components/ExtraFeatures/Passphrase.js | 34 +- .../js/Clipperz/PM/UI/Components/Overlay.js | 23 +- .../Components/Panels/ExtraFeaturesPanel.js | 45 +- .../js/Clipperz/PM/UI/ExportController.js | 437 +++++++++--------- .../delta/js/Clipperz/PM/UI/MainController.js | 28 +- frontend/delta/js/FileSaver/Blob.js | 234 ++++++++++ frontend/delta/js/FileSaver/FileSaver.js | 271 +++++++++++ .../delta/properties/delta.properties.json | 3 + frontend/delta/scss/core/layout.scss | 19 +- frontend/delta/scss/core/overlay.scss | 17 + frontend/delta/scss/style/settingsPanel.scss | 188 +++++--- 18 files changed, 1195 insertions(+), 474 deletions(-) create mode 100644 frontend/delta/js/FileSaver/Blob.js create mode 100644 frontend/delta/js/FileSaver/FileSaver.js diff --git a/frontend/delta/css/clipperz.css b/frontend/delta/css/clipperz.css index 2c6fab1..16b9b6a 100644 --- a/frontend/delta/css/clipperz.css +++ b/frontend/delta/css/clipperz.css @@ -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,65 +2115,88 @@ span.count { #extraFeaturesPanel .extraFeatureContent .extraFeature h1 { font-size: 20pt; padding-bottom: 20px; } - #extraFeaturesPanel .extraFeatureContent form label { - display: none; } - #extraFeaturesPanel .extraFeatureContent form input { - display: block; - font-size: 18pt; - margin-bottom: 8px; - padding: 6px 10px; - border: 0px solid white; - width: 350px; - color: black; } - #extraFeaturesPanel .extraFeatureContent form input.invalid { - border: 0px solid #ff9900; - color: gray; } - #extraFeaturesPanel .extraFeatureContent form p { - display: -webkit-box; - display: -webkit-flex; - display: -moz-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-direction: normal; - -webkit-box-orient: horizontal; - -webkit-flex-direction: row; - -moz-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; } - #extraFeaturesPanel .extraFeatureContent form p input { - width: 30px; - -webkit-box-flex: auto; - -webkit-flex: auto; - -moz-box-flex: auto; - -moz-flex: auto; - -ms-flex: auto; - flex: auto; } - #extraFeaturesPanel .extraFeatureContent form p span { - -webkit-box-flex: auto; - -webkit-flex: auto; - -moz-box-flex: auto; - -moz-flex: auto; - -ms-flex: auto; - flex: auto; - font-size: 12pt; } - #extraFeaturesPanel .extraFeatureContent form button { - font-family: "clipperz-font"; - color: white; - font-size: 14pt; - border: 0px; - margin-top: 20px; - padding: 6px 10px; - border: 1px solid white; - background-color: #ff9900; - -webkit-transition: background-color font-weight 0.2s linear; - -moz-transition: background-color font-weight 0.2s linear; - -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 { - font-weight: 100; - background-color: #c0c0c0; - cursor: default; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form label { + display: none; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form input { + display: block; + font-size: 18pt; + margin-bottom: 8px; + padding: 6px 10px; + border: 0px solid white; + width: 350px; + color: black; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form input.invalid { + border: 0px solid #ff9900; + color: gray; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form p { + display: -webkit-box; + display: -webkit-flex; + display: -moz-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-direction: normal; + -webkit-box-orient: horizontal; + -webkit-flex-direction: row; + -moz-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form p input { + width: 30px; + -webkit-box-flex: auto; + -webkit-flex: auto; + -moz-box-flex: auto; + -moz-flex: auto; + -ms-flex: auto; + flex: auto; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form p span { + -webkit-box-flex: auto; + -webkit-flex: auto; + -moz-box-flex: auto; + -moz-flex: auto; + -ms-flex: auto; + flex: auto; + font-size: 12pt; } + #extraFeaturesPanel .extraFeatureContent .extraFeature form button { + font-family: "clipperz-font"; + color: white; + font-size: 14pt; + border: 0px; + margin-top: 20px; + padding: 6px 10px; + border: 1px solid white; + background-color: #ff9900; + -webkit-transition: background-color font-weight 0.2s linear; + -moz-transition: background-color font-weight 0.2s linear; + -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 .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; diff --git a/frontend/delta/html/index_template.html b/frontend/delta/html/index_template.html index 6f6c166..4514f08 100644 --- a/frontend/delta/html/index_template.html +++ b/frontend/delta/html/index_template.html @@ -85,6 +85,7 @@ Clipperz_normalizedNewLine = '\x0d\x0a'; + loading diff --git a/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js b/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js index da960b9..db91fad 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/Record.Version.js @@ -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(); diff --git a/frontend/delta/js/Clipperz/PM/DataModel/Record.js b/frontend/delta/js/Clipperz/PM/DataModel/Record.js index aa3311b..5abe8c5 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/Record.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/Record.js @@ -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) { @@ -1184,7 +1151,7 @@ console.log("Record.hasPendingChanges RESULT", result); return previous; }, {}); - result = Clipperz.Async.collectResults("Record.exportDirectLogins",callbackObject,{trace:false})(); + result = Clipperz.Async.collectResults("Record.exportDirectLogins", callbackObject,{trace:false})(); } return result; @@ -1202,20 +1169,20 @@ console.log("Record.hasPendingChanges RESULT", result); currentVersion = {}; directLogins = {}; deferredResult = new Clipperz.Async.Deferred('Record.export', {trace:false}); - deferredResult.addMethod(this,'getCurrentRecordVersion'); + deferredResult.addMethod(this, 'getCurrentRecordVersion'); deferredResult.addCallback(function(recordVersionIn) { currentVersionObject = recordVersionIn; }) - deferredResult.addMethod(this,'fullLabel'); - deferredResult.addMethod(this,function(labelIn) {label = labelIn}); - deferredResult.addMethod(this,'exportDirectLogins'); + deferredResult.addMethod(this, 'fullLabel'); + deferredResult.addMethod(this, function(labelIn) {label = labelIn}); + 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,'notes'); - deferredResult.addMethod(this,function(notesIn) { data['notes'] = notesIn; }); - deferredResult.addMethod(this,function() { currentVersion['reference'] = this.currentVersionReference(); }); +// 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.addCallback(function() { return currentVersionObject.exportFields(); }), deferredResult.addCallback(function(fieldsIn) { currentVersion['fields'] = fieldsIn; }); - deferredResult.addMethod(this,function() { + deferredResult.addMethod(this, function() { return { 'label': label, 'data': data, @@ -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; +}; diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js index 13ebe3f..fb2ceb4 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js @@ -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 it’s 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"), + ]) +*/ + ]) + ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js index 3c71f0d..fc1d976 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js @@ -78,19 +78,21 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({ return React.DOM.div({className:'extraFeature deleteAccount'}, [ React.DOM.h1({}, "Delete Account"), - 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"), - React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}), - React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"), - React.DOM.input({'key':'passphrase', 'className':this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}), - React.DOM.p({}, [ - React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}), - React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.") + 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"), + React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}), + React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"), + React.DOM.input({'key':'passphrase', 'className':this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}), + React.DOM.p({}, [ + React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}), + React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.") + ]), ]), - ]), - 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) + 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) + ]) ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js index 45c835e..9c8e938 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js @@ -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.h3({}, this.props['PIN']) + React.DOM.div({'className': 'content'}, [ + React.DOM.h3({}, this.props['PIN']) + ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js index a286d3a..49f7aeb 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js @@ -101,27 +101,29 @@ Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({ render: function () { return React.DOM.div({className:'extraFeature passphrase'}, [ React.DOM.h1({}, "Change Passphrase"), - 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"), - React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}), + 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"), + React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}), - React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"), - React.DOM.input({'key':'old-passphrase', 'className':this.state['old-passphrase'], 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}), + React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"), + React.DOM.input({'key':'old-passphrase', 'className':this.state['old-passphrase'], 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}), - React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"), - React.DOM.input({'key':'new-passphrase', 'className':this.state['new-passphrase'], 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}), + React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"), + React.DOM.input({'key':'new-passphrase', 'className':this.state['new-passphrase'], 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}), - React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"), - React.DOM.input({'key':'confirm-new-passphrase', 'className':this.state['confirm-new-passphrase'], 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"}), + React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"), + React.DOM.input({'key':'confirm-new-passphrase', 'className':this.state['confirm-new-passphrase'], 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"}), - React.DOM.p({}, [ - React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}), - React.DOM.span({}, "I understand that Clipperz will not be able to recover a lost passphrase.") + React.DOM.p({}, [ + React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}), + React.DOM.span({}, "I understand that Clipperz will not be able to recover a lost passphrase.") + ]), ]), - ]), - React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"), - ]), + React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"), + ]) + ]) ]); }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js index f9f6bcd..01d8a77 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js @@ -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; }, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js index 92609e5..f27ccd7 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js @@ -70,9 +70,13 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({ //========================================================================= - showExtraFeatureComponent: function (aComponentName) { + toggleExtraFeatureComponent: function (aComponentName) { return MochiKit.Base.bind(function () { - this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName); + 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, ]); } diff --git a/frontend/delta/js/Clipperz/PM/UI/ExportController.js b/frontend/delta/js/Clipperz/PM/UI/ExportController.js index 11c5207..a4bd81c 100644 --- a/frontend/delta/js/Clipperz/PM/UI/ExportController.js +++ b/frontend/delta/js/Clipperz/PM/UI/ExportController.js @@ -21,87 +21,111 @@ refer to http://www.clipperz.com. */ +"use strict"; Clipperz.Base.module('Clipperz.PM.UI'); +// https://github.com/eligrey/FileSaver.js +// https://github.com/eligrey/Blob.js + 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(); + this._processedRecords = 0; - this._style = "body {"+ - " margin: 0;"+ - " padding: 0;"+ - " font-family: monospace;"+ - "}"+ - ""+ - "p {"+ - " padding-left: 1em;"+ - "}"+ - ""+ - "h1 {"+ - " color: #ff9900;"+ - " background: black;"+ - " box-shadow: 0px 5px 6px 0 rgba(0, 0, 0, 0.15);"+ - " margin: 0;"+ - " padding:1em;"+ - "}"+ - ""+ - ".progressBar {"+ - " position: absolute;"+ - " width: 100%;"+ - " margin-top: 0px;"+ - " "+ - "}"+ - ""+ - "#completed {"+ - " background: #ff9900;"+ - " color: white;"+ - " width: 0;"+ - " overflow: hidden;"+ - " font-size: 0.8em;"+ - " box-shadow: 0px 4px 6px 0 rgba(0, 0, 0, 0.15);"+ - "}"+ - ""+ - "#printableUl {"+ - " width:100%;"+ - " height:80%;"+ - " margin: 0;"+ - " padding: 0;"+ - " list-style-type: none;"+ - "}"+ - ""+ - "#printableUl li {"+ - " border: 1px solid #1863a1;"+ - " margin: 1em;"+ - ""+ - "}"+ - ""+ - "#printableUl li .label {"+ - " background: #1863a1;"+ - " color: white;"+ - " display: block;"+ - " padding: 1em;"+ - "}"+ - ""+ - "#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;"+ - "}"+ - ""; + this._style = + "body {" + + "font-family: 'Dejavu Sans', monospace;" + + "margin: 0px;" + + "}" + + + "header {" + + "padding: 10px;" + + "border-bottom: 2px solid black;" + + "}" + + + "h1 {" + + "margin: 0px;" + + "}" + + + "h2 {" + + "margin: 0px;" + + "padding-top: 10px;" + + "}" + + + "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;" + + "padding: 10px;" + + "}" + + + "textarea {" + + "width: 100%;" + + "height: 200px;" + + "}" + + + "@media print {" + + "div > div, header > div {" + + "display: none !important;" + + "}" + + + "ul > li {" + + "page-break-inside: avoid;" + + "} " + + "}" + + + ""; 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; - }, - - 'setWindowBody': function (aWindow, anHTML) { - aWindow.document.body.innerHTML = anHTML; + '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)); + + 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({}, + MochiKit.Base.map(function(key) { + return [ + MochiKit.DOM.DT(jsonCardData.currentVersion.fields[key].label), + MochiKit.DOM.DD(jsonCardData.currentVersion.fields[key].value), + ]; + }, MochiKit.Base.keys(jsonCardData.currentVersion.fields)) + ) + ), + jsonCardData.data.notes ? MochiKit.DOM.P({}, jsonCardData.data.notes) : null + ); }, - //============================================================================= + 'renderToHtml': function (jsonData) { + var title; + var style; + var date; + var body; - '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'},"") ) + title = "Clipperz data"; + style = this._style; + date = "dd/mm/yyyy"; + + 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.") + ) + ), + + 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") ) ) ); - - 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)); + + return '' + title + '' + MochiKit.DOM.toHTML(body) + ''; }, - //----------------------------------------------------------------------------- - - '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)) - ); + //---------------------------------------------------------------------------- + + 'saveResult': function (exportedJSON) { + var blob; + var sortedJSON; - 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, - MochiKit.DOM.DL({}, - Object.keys(card.currentVersion.fields).map(function(key) { - return [ - MochiKit.DOM.DT(card.currentVersion.fields[key].label), - MochiKit.DOM.DD(card.currentVersion.fields[key].value), - ]; - }) - ) - ); - }) - ) - ); - - this.setWindowTitle(aWindow, "Clipperz Exported Data"); - this.setWindowBody(aWindow, MochiKit.DOM.toHTML(dom)); - }, - - 'updateWindowError': function (aWindow, errorMessage) { - this.setWindowBody(aWindow, - "

Error

"+ - "

The following error occured while exporting your data:

"+ - ""+errorMessage+"" - ); + sortedJSON = MochiKit.Iter.sorted(exportedJSON, function(a,b) { return a.label.toUpperCase().localeCompare(b.label.toUpperCase()); } ); + + blob = new Blob([this.renderToHtml(sortedJSON)], {type: "text/html;charset=utf-8"}); + saveAs(blob, "clipperz_data.html"); }, //============================================================================= - - 'runExportJSON': function (aWindow) { + + 'run': function () { var deferredResult; - var exportedRecords; - - var totalRecords = this.recordsInfo().length; - - exportedRecords = 0; + var self = this; - 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); - - 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+'%'; - - return exportedRecord; - })); - dr.callback(); - return dr; + deferredResult = new Clipperz.Async.Deferred("ExportController.run", {trace:false}); + deferredResult.addCallback(MochiKit.Base.map, function(recordIn) { + var innerDeferredResult; + + 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, 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(); + deferredResult.addMethod(this, 'saveResult'); + deferredResult.callback(this.recordsInfo()); return deferredResult; }, - - //============================================================================= - - 'run': function () { - var newWindow; - - newWindow = window.open("", this.target()); - - return this.runExportJSON(newWindow); - }, - - //============================================================================= - - '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); - }, //============================================================================= __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(); -}; diff --git a/frontend/delta/js/Clipperz/PM/UI/MainController.js b/frontend/delta/js/Clipperz/PM/UI/MainController.js index 7b8a5cb..b26adbb 100644 --- a/frontend/delta/js/Clipperz/PM/UI/MainController.js +++ b/frontend/delta/js/Clipperz/PM/UI/MainController.js @@ -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; }, //---------------------------------------------------------------------------- diff --git a/frontend/delta/js/FileSaver/Blob.js b/frontend/delta/js/FileSaver/Blob.js new file mode 100644 index 0000000..2e2d360 --- /dev/null +++ b/frontend/delta/js/FileSaver/Blob.js @@ -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)); \ No newline at end of file diff --git a/frontend/delta/js/FileSaver/FileSaver.js b/frontend/delta/js/FileSaver/FileSaver.js new file mode 100644 index 0000000..c43c5f6 --- /dev/null +++ b/frontend/delta/js/FileSaver/FileSaver.js @@ -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; + }); +} \ No newline at end of file diff --git a/frontend/delta/properties/delta.properties.json b/frontend/delta/properties/delta.properties.json index bbfa4a4..b53c099 100644 --- a/frontend/delta/properties/delta.properties.json +++ b/frontend/delta/properties/delta.properties.json @@ -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", diff --git a/frontend/delta/scss/core/layout.scss b/frontend/delta/scss/core/layout.scss index ce969d3..271cbf0 100644 --- a/frontend/delta/scss/core/layout.scss +++ b/frontend/delta/scss/core/layout.scss @@ -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(); + } + } } } diff --git a/frontend/delta/scss/core/overlay.scss b/frontend/delta/scss/core/overlay.scss index 5ba1c05..ce5ea83 100644 --- a/frontend/delta/scss/core/overlay.scss +++ b/frontend/delta/scss/core/overlay.scss @@ -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); + } + } } //======================================================== diff --git a/frontend/delta/scss/style/settingsPanel.scss b/frontend/delta/scss/style/settingsPanel.scss index a5126e9..8a8ce7a 100644 --- a/frontend/delta/scss/style/settingsPanel.scss +++ b/frontend/delta/scss/style/settingsPanel.scss @@ -86,6 +86,10 @@ refer to http://www.clipperz.com. & > div { padding: 4px; } + + &.offlineCopy { + cursor: default; + } } &.open { @@ -178,6 +182,118 @@ refer to http://www.clipperz.com. font-size: 20pt; padding-bottom: 20px; } + + form { + + label { + display: none; + } + + input { + $border-size: 0px; // 2px; + + display: block; + font-size: 18pt; + margin-bottom: 8px; + padding: (6px - $border-size) (10px - $border-size); + border: $border-size solid white; + width: 350px; + color: black; + + &.invalid { + border: $border-size solid $clipperz-orange; + color: gray; + } + } + + p { + @include flexbox; + @include flex-direction(row); + + input { + width: 30px; + @include flex(auto); + } + + span { + @include flex(auto); + font-size: 12pt; + } + } + + button { + font-family: "clipperz-font"; + + color: white; + font-size: 14pt; + border: 0px; + + margin-top: 20px; + padding: 6px 10px; + + border: 1px solid white; + background-color: $main-color; + @include transition(background-color font-weight, 0.2s, linear); + + &:hover { + }; + + &:disabled { + font-weight: 100; + background-color: #c0c0c0; + cursor: default; + + &:hover { + }; + } + } +// input.valid:focus { +// border: 2px solid $clipperz-blue; +// } + } + + 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 { + }; + } } @@ -211,78 +327,6 @@ refer to http://www.clipperz.com. } */ - form { - - label { - display: none; - } - - input { - $border-size: 0px; // 2px; - - display: block; - font-size: 18pt; - margin-bottom: 8px; - padding: (6px - $border-size) (10px - $border-size); - border: $border-size solid white; - width: 350px; - color: black; - - &.invalid { - border: $border-size solid $clipperz-orange; - color: gray; - } - } - - p { - @include flexbox; - @include flex-direction(row); - - input { - width: 30px; - @include flex(auto); - } - - span { - @include flex(auto); - font-size: 12pt; - } - } - - 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; - padding: 6px 10px; - - border: 1px solid white; - background-color: $main-color; - @include transition(background-color font-weight, 0.2s, linear); - - &:hover { - }; - - &:disabled { - font-weight: 100; - background-color: #c0c0c0; - cursor: default; - - &:hover { - }; - } - } -// input.valid:focus { -// border: 2px solid $clipperz-blue; -// } - } } }