1
0
mirror of http://git.whoc.org.uk/git/password-manager.git synced 2025-10-25 17:47:34 +02:00

Implemented Attachments in client

This commit is contained in:
Dario Chiappetta
2015-11-23 16:10:44 +01:00
parent 8608fb4253
commit 8c59393433
50 changed files with 4862 additions and 272 deletions

View File

@@ -0,0 +1,550 @@
/*
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/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI');
Clipperz.PM.UI.AttachmentController = function(someParameters) {
this.MAX_SIMULTANEOUS_READ = 1;
this.MAX_SIMULTANEOUS_UPLOAD = 1;
this.MAX_SIMULTANEOUS_DOWNLOAD = 1;
this.MAX_SIMULTANEOUS_ENCRYPT = 1;
this.MAX_SIMULTANEOUS_DECRYPT = 1;
this.LATEST_ENCRYPTION_VERSION = '1.0'; // Versions aren't handled completely yet!
this.fileQueue = [];
this.notifications = [];
this.operationsCount = null;
this.encryptedDocument = null;
this.uploadMessageCallback = someParameters['uploadMessageCallback'];
this.downloadMessageCallback = someParameters['downloadMessageCallback'];
this.reloadServerStatusCallback = someParameters['reloadServerStatusCallback'];
return this;
}
MochiKit.Base.update(Clipperz.PM.UI.AttachmentController.prototype, {
toString: function () {
return "Clipperz.PM.UI.AttachmentController";
},
//-------------------------------------------------------------------------
notifyUpdate: function() {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updateAttachmentQueueInfo', this.getQueueInfo(), this.getNotificationsInfo());
},
getQueueInfo: function() {
return this.fileQueue;
},
getNotificationsInfo: function() {
return this.notifications;
},
//=========================================================================
// Entry points
//=========================================================================
addAttachment: function (anAttachment) {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.uploadAttachment", {trace:false});
deferredResult.collectResults({
'_attachment': MochiKit.Base.partial(MochiKit.Async.succeed, anAttachment),
'_record': MochiKit.Base.method(anAttachment, 'record'),
'reference': MochiKit.Base.method(anAttachment, 'reference'),
'meta': MochiKit.Base.method(anAttachment, 'metadata'),
'key': MochiKit.Base.method(anAttachment, 'key'),
'nonce': MochiKit.Base.method(anAttachment, 'nonce'),
'status': MochiKit.Base.partial(MochiKit.Async.succeed, 'WAITING_READ'),
'file': MochiKit.Base.method(anAttachment, 'file'),
'recordReference': MochiKit.Base.method(anAttachment.record(), 'reference'),
'process': MochiKit.Base.partial(MochiKit.Async.succeed, 'UPLOAD'), // Used only to differentiate notifications
}, {trace: false});
deferredResult.addMethod(this, 'addFileToQueue');
deferredResult.callback();
return deferredResult;
},
getAttachment: function(anAttachment, aMessageCallback) {
if (this.getQueuePosition(anAttachment.reference()) >= 0) {
this.removeFileFromQueue(anAttachment.reference());
}
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.downloadAttachment", {trace:false});
deferredResult.collectResults({
'_attachment': MochiKit.Base.partial(MochiKit.Async.succeed, anAttachment),
'_record': MochiKit.Base.method(anAttachment, 'record'),
'reference': MochiKit.Base.method(anAttachment, 'reference'),
'meta': MochiKit.Base.method(anAttachment, 'metadata'),
'key': MochiKit.Base.method(anAttachment, 'key'),
'nonce': MochiKit.Base.method(anAttachment, 'nonce'),
'status': MochiKit.Base.partial(MochiKit.Async.succeed, 'WAITING_DOWNLOAD'),
'messageCallback': MochiKit.Async.succeed(aMessageCallback),
'process': MochiKit.Base.partial(MochiKit.Async.succeed, 'DOWNLOAD'), // Used only to differentiate notifications
}, {trace: false});
deferredResult.addCallback(function(aResult){
MochiKit.Base.update(aResult, {'messageCallback': aMessageCallback});
return aResult;
});
deferredResult.addMethod(this, 'addFileToQueue');
deferredResult.callback();
return deferredResult;
},
cancelAttachment: function(anAttachment) {
var deferredResult;
var reference = anAttachment.reference()
var queueElement = this.getQueueElement(reference);
if (queueElement) {
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.cancelAttachment", {trace:false});
deferredResult.addMethod(this, 'updateFileInQueue', reference, {'status': 'CANCELED'});
if (queueElement['deferredRequest']) {
deferredResult.addMethod(queueElement['deferredRequest'], 'cancel');
}
deferredResult.callback();
// TODO: We may also want do delete stuff in the queue element
} else {
deferredResult = MochiKit.Async.succeed();
}
return deferredResult;
},
//=========================================================================
// Queue management
//=========================================================================
dispatchQueueOperations: function() {
var currentElement;
var processNextElements;
var count = this.updateOperationsCount();
this.notifyUpdate();
processNextElements = true;
for (i in this.fileQueue) {
if(processNextElements) {
currentElement = this.fileQueue[i];
switch (currentElement['status']) {
case 'WAITING_READ':
if ((count['READING']) < this.MAX_SIMULTANEOUS_READ) {
this.readFile(currentElement['reference'], currentElement['file']);
processNextElements = false;
}
break;
case 'WAITING_ENCRYPT':
if (count['ENCRYPTING'] < this.MAX_SIMULTANEOUS_ENCRYPT) {
this.encryptFile(currentElement['reference'], currentElement['originalArray'], currentElement['key'], currentElement['nonce']);
processNextElements = false;
}
break;
case 'WAITING_UPLOAD':
if (count['UPLOADING'] < this.MAX_SIMULTANEOUS_UPLOAD) {
this.uploadFile(currentElement['reference'], currentElement['encryptedArray']);
processNextElements = false;
}
break;
case 'WAITING_DOWNLOAD':
if (count['DOWNLOADING'] < this.MAX_SIMULTANEOUS_DOWNLOAD) {
this.downloadFile(currentElement['reference'], currentElement['messageCallback']);
processNextElements = false;
}
break;
case 'WAITING_DECRYPT':
if (count['DECRYPTING'] < this.MAX_SIMULTANEOUS_DECRYPT) {
this.decryptFile(currentElement['reference'], currentElement['encryptedArray'], currentElement['key'], currentElement['nonce']);
processNextElements = false;
}
break;
case 'WAITING_SAVE':
this.saveFile(currentElement['reference'], currentElement['decryptedArray'], currentElement['meta']['name'], currentElement['meta']['type']);
processNextElements = false;
Clipperz.Sound.beep();
break;
}
}
}
},
updateOperationsCount: function() {
var count;
count = {
'WAITING_READ': 0,
'READING': 0,
'WAITING_ENCRYPT': 0,
'ENCRYPTING': 0,
'WAITING_UPLOAD': 0,
'UPLOADING': 0,
'WAITING_DOWNLOAD': 0,
'DOWNLOADING': 0,
'WAITING_DECRYPT': 0,
'DECRYPTING': 0,
'WAITING_SAVE': 0,
'DONE': 0,
'CANCELED': 0,
'FAILED': 0,
};
for (var i in this.fileQueue) {
count[this.fileQueue[i]['status']]++;
}
this.operationsCount = count;
return this.operationsCount;
},
addFileToQueue: function(someParameters) {
this.fileQueue.push(someParameters);
this.addNotification(someParameters);
this.dispatchQueueOperations();
},
removeFileFromQueue: function(aFileReference) {
this.fileQueue.splice(this.getQueuePosition(aFileReference), 1);
this.dispatchQueueOperations();
},
getQueueElement: function(aFileReference) {
var i = this.getQueuePosition(aFileReference);
return this.fileQueue[i];
},
updateFileInQueue: function(aFileReference, someParameters) {
var queuePosition = this.getQueuePosition(aFileReference);
MochiKit.Base.update(this.fileQueue[queuePosition], someParameters);
this.dispatchQueueOperations();
},
appendResult: function(aFileReference, anArray) {
var queueElement = this.getQueueElement(aFileReference);
queueElement['result'].set(anArray, queueElement['currentByte']);
},
getQueuePosition: function(aFileReference) {
var result;
result = -1;
for (var i in this.fileQueue) {
if (this.fileQueue[i].reference == aFileReference) {
result = i;
}
}
return result;
},
//=========================================================================
// Queue Processing
//=========================================================================
addNotification: function(aQueueElement) {
this.notifications.push({
'id': this.randomId(),
'queueElement': aQueueElement
})
},
removeNotification: function(aNotificationId) {
var i, position;
position = -1;
for (i in this.notifications) {
if (this.notifications[i]['id'] == aNotificationId) {
position = i;
}
}
if (position >= 0) {
this.notifications.splice(position, 1);
}
this.notifyUpdate();
},
randomId: function() {
return Clipperz.Crypto.PRNG.defaultRandomGenerator().getRandomBytes(32).toHexString().substring(2);
},
//=========================================================================
// Queue Processing: READ
//=========================================================================
readFile: function(aFileReference, aFile) {
var reader = new FileReader();
this.updateFileInQueue(aFileReference, {
'status': 'READING',
});
reader.onload = MochiKit.Base.method(this, 'readFileOnload', aFileReference);
reader.readAsArrayBuffer(aFile);
},
readFileOnload: function(aFileReference, anEvent) {
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_ENCRYPT',
'originalArray': new Uint8Array(anEvent.target.result),
})
},
//=========================================================================
// Queue Processing: ENCRYPT
//=========================================================================
encryptFile: function(aFileReference, anArrayBuffer, aKey, aNonce) {
this.updateFileInQueue(aFileReference, {
'status': 'ENCRYPTING',
});
window.crypto.subtle.importKey(
"raw",
aKey, //this is an example jwk key, "raw" would be an ArrayBuffer
{ name: "AES-CBC" }, //this is the algorithm options
false, //whether the key is extractable (i.e. can be used in exportKey)
["encrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
)
// .then(MochiKit.Base.method(this, 'doEncrypt', aFileReference, anArrayBuffer, aNonce))
.then(this.doEncrypt.bind(this,aFileReference, anArrayBuffer, aNonce))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'encryptFile(): encryption failed'));
},
doEncrypt: function(aFileReference, anArrayBuffer, anIV, aWebcryptoKey) {
window.crypto.subtle.encrypt(
{
name: "AES-CBC",
iv: anIV,
},
aWebcryptoKey,
anArrayBuffer
)
.then(MochiKit.Base.method(this, 'doneEncrypt', aFileReference))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'doEncrypt(): encryption failed'));
},
doneEncrypt: function(aFileReference, anArrayBuffer) {
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_UPLOAD',
'encryptedArray': new Uint8Array(anArrayBuffer),
});
},
//=========================================================================
// Queue Processing: UPLOAD
//=========================================================================
uploadFile: function(aFileReference, anEncryptedArray) {
this.updateFileInQueue(aFileReference, {
'status': 'UPLOADING',
'deferredRequest': this.uploadFileRequest(aFileReference, anEncryptedArray),
'requestProgress': 0,
});
},
uploadFileRequest: function(aFileReference, anEncryptedArray) {
var deferredResult;
var queueElement = this.getQueueElement(aFileReference);
deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.uploadFileRequest", {trace:false});
deferredResult.addCallback(this.uploadMessageCallback, {
'attachmentReference': queueElement['_attachment'].reference(),
'recordReference': queueElement['_attachment'].record().reference(),
'arrayBufferData': anEncryptedArray,
'version': this.LATEST_ENCRYPTION_VERSION,
}, MochiKit.Base.method(this, 'uploadFileProgress', aFileReference));
deferredResult.addMethod(this, 'uploadFileDone', aFileReference);
deferredResult.addErrback(MochiKit.Base.method(this, 'handleException', aFileReference, 'uploadFileRequest(): request failed or canceled'));
deferredResult.callback();
return deferredResult;
},
uploadFileDone: function(aFileReference, aResult){
var record = this.getQueueElement(aFileReference)['_record'];
return Clipperz.Async.callbacks("AttachmentController.uploadFileDone", [
MochiKit.Base.partial(this.reloadServerStatusCallback, record),
MochiKit.Base.method(this, 'updateFileInQueue', aFileReference, {
'status': 'DONE',
'requestProgress': 1,
}),
], {trace:false});
},
uploadFileProgress: function(aFileReference, anEvent) {
var newProgress = (anEvent.lengthComputable) ? (anEvent.loaded / anEvent.total) : -1;
this.updateFileInQueue(aFileReference, {
'requestProgress': newProgress,
});
},
//=========================================================================
// Queue Processing: DOWNLOAD
//=========================================================================
downloadFile: function(aFileReference) {
var deferredRequest;
var queueElement = this.getQueueElement(aFileReference);
deferredRequest = new Clipperz.Async.Deferred("Clipperz.PM.UI.AttachmentController.downloadFile", {trace:false});
deferredRequest.addCallback(this.downloadMessageCallback, queueElement['_attachment'], MochiKit.Base.method(this, 'downloadFileProgress', aFileReference));
deferredRequest.addMethod(this, 'downloadFileDone', aFileReference);
deferredRequest.addErrback(MochiKit.Base.method(this, 'handleException', aFileReference, 'downloadFile(): download filed or canceled'));
deferredRequest.callback();
this.updateFileInQueue(aFileReference, {
'status': 'DOWNLOADING',
'deferredRequest': deferredRequest,
'requestProgress': 0,
});
},
downloadFileDone: function(aFileReference, aResult){
var queueElement = this.getQueueElement(aFileReference);
var encryptedArray = new Uint8Array(aResult);
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_DECRYPT',
'key': queueElement['key'],
'nonce': queueElement['nonce'],
'encryptedArray': encryptedArray,
'requestProgress': 1,
});
},
downloadFileProgress: function(aFileReference, anEvent) {
var newProgress = (anEvent.lengthComputable) ? (anEvent.loaded / anEvent.total) : -1;
this.updateFileInQueue(aFileReference, {
'requestProgress': newProgress,
});
},
//=========================================================================
// Queue Processing: DECRYPT
//=========================================================================
decryptFile: function(aFileReference, anArrayBuffer, aKey, aNonce) {
this.updateFileInQueue(aFileReference, {
'status': 'DECRYPTING',
});
window.crypto.subtle.importKey(
"raw",
aKey, //this is an example jwk key, "raw" would be an ArrayBuffer
{name: "AES-CBC"}, //this is the algorithm options
false, //whether the key is extractable (i.e. can be used in exportKey)
["decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
)
.then(MochiKit.Base.method(this, 'doDecrypt', aFileReference, anArrayBuffer, aNonce))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'decryptFile(): decryption failed'));
},
doDecrypt: function(aFileReference, anArrayBuffer, anIV, aWebcryptoKey) {
window.crypto.subtle.decrypt(
{name: "AES-CBC", iv: anIV},
aWebcryptoKey,
anArrayBuffer
)
.then(MochiKit.Base.method(this, 'doneDecrypt', aFileReference))
.catch(MochiKit.Base.method(this, 'handleException', aFileReference, 'doDecrypt(): decryption failed'));
},
doneDecrypt: function(aFileReference, anArrayBuffer) {
this.updateFileInQueue(aFileReference, {
'status': 'WAITING_SAVE',
'decryptedArray': new Uint8Array(anArrayBuffer),
});
},
//=========================================================================
// Queue Processing: SAVE
//=========================================================================
saveFile: function(aFileReference, anArray, aFileName, aFileType) {
var blob = new Blob([anArray], {type: aFileType});
saveAs(blob, aFileName);
this.updateFileInQueue(aFileReference, {
'status': 'DONE',
});
},
//=========================================================================
// Exceptions
//=========================================================================
/** Handles exceptions for upload/download and encrypt/decrypt. Note that
* an exception is thrown also when the user manually cancels the file
* processing. In this case the status remains 'CANCELED'.
*/
handleException: function(aFileReference, aMessage) {
var queueElement = this.getQueueElement(aFileReference);
if (queueElement['status'] != 'CANCELED') {
this.updateFileInQueue(aFileReference, {
'status': 'FAILED',
});
}
if (aMessage) {
console.log("AttachmentController: caught exception (" + aMessage + ")");
}
},
//=========================================================================
__syntaxFix__: "syntax fix"
});

View File

@@ -0,0 +1,119 @@
/*
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/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components');
Clipperz.PM.UI.Components.AttachmentQueueBoxClass = React.createClass({
propTypes: {
'attachmentQueueInfo': React.PropTypes.object.isRequired,
'attachmentQueueBoxStatus': React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
'unreadNotifications': [],
};
},
//=========================================================================
// Actions
//=========================================================================
handleClose: function(aNotificationElement) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'closeAttachmentNotification', aNotificationElement['id']);
},
//=========================================================================
// Render Methods
//=========================================================================
renderNotificationElement: function(aNotificationElement) {
var processIcon, status, closeButton, progressIndicator;
var queueElement = aNotificationElement['queueElement'];
processIcon = (queueElement['process'] == 'UPLOAD') ? "\u2b06" : "\u2b07";
status = "waiting";
closeButton = null;
progressIndicator = null;
if (queueElement['status'] == 'DOWNLOADING' || queueElement['status'] == 'UPLOADING') {
status = Math.floor(queueElement['requestProgress']*100) + '%';
progressIndicator = React.DOM.span({'className': 'progress'}, Clipperz.PM.UI.Components.RadialProgressIndicator({'progress': queueElement['requestProgress']}));
}
if (queueElement['status'] == 'DONE' || queueElement['status'] == 'CANCELED' || queueElement['status'] == 'FAILED') {
status = (queueElement['status'] == 'DONE') ? 'completed' : (queueElement['status'] == 'FAILED') ? "failed" : "canceled";
closeButton = React.DOM.span({'className': 'close'}, React.DOM.a({
'className': 'close',
'onClick': MochiKit.Base.method(this, 'handleClose', aNotificationElement),
}, "remove field"));
}
return React.DOM.li({},[
React.DOM.span({'className': 'contentType'}, Clipperz.PM.DataModel.Attachment.contentTypeIcon(queueElement['meta']['type'])),
React.DOM.span({'className': 'name'}, queueElement['meta']['name']),
// React.DOM.span({'className': 'size'}, queueElement['meta']['size']),
React.DOM.span({'className': 'status'}, [
React.DOM.span({'className': 'statusString'}, status),
React.DOM.span({'className': 'processIcon'}, processIcon),
]),
progressIndicator,
closeButton,
]);
},
renderNotifications: function(someNotifications) {
var result;
if (someNotifications.length == 0) {
result = React.DOM.p({}, "No attachments in queue");
} else {
result = MochiKit.Base.map(MochiKit.Base.method(this, 'renderNotificationElement'), someNotifications);
}
return result;
},
// renderNotifications: function(someNotifications) {
// console.log('AttachmentQueueBox.renderNotifications:', someNotifications);
// },
render: function () {
//test
this.renderNotifications(this.props['attachmentQueueInfo']['notifications']);
return React.DOM.div({
'className': 'attachmentQueueStatus '+this.props['attachmentQueueBoxStatus'],
}, [
React.DOM.div({'className': 'arrow'}),
React.DOM.ul({}, this.renderNotifications(this.props['attachmentQueueInfo']['notifications'])),
]);
}
//=========================================================================
});
Clipperz.PM.UI.Components.AttachmentQueueBox = React.createFactory(Clipperz.PM.UI.Components.AttachmentQueueBoxClass);

View File

@@ -30,22 +30,40 @@ Clipperz.PM.UI.Components.ButtonClass = React.createClass({
'eventName': React.PropTypes.string.isRequired,
'label': React.PropTypes.string.isRequired,
'handler': React.PropTypes.func.isRequired,
'className': React.PropTypes.string
'className': React.PropTypes.string,
'badgeTopContent': React.PropTypes.string,
'badgeBottomContent': React.PropTypes.string,
},
//=========================================================================
render: function () {
var badgeTop;
var badgeBottom;
var classes = {
'button': true
};
if (typeof(this.props['className']) != 'undefined') {
classes[this.props['className']] = true;
};
badgeTop = null;
if (this.props['badgeTopContent']) {
badgeTop = React.DOM.span({'className': 'badge top'}, this.props['badgeTopContent']);
};
badgeBottom = null;
if (this.props['badgeBottomContent']) {
badgeBottom = React.DOM.span({'className': 'badge bottom'}, this.props['badgeBottomContent']);
};
return React.DOM.div({className:Clipperz.PM.UI.Components.classNames(classes), onClick:this.props['handler']}, [
React.DOM.div({className:this.props['eventName']}, [
React.DOM.h3({className:'label'}, this.props['label'])
React.DOM.h3({className:'label'}, this.props['label']),
badgeTop,
badgeBottom,
])
]);
}

View File

@@ -32,7 +32,8 @@ Clipperz.PM.UI.Components.CardToolbarClass = React.createClass({
'accountInfo': React.PropTypes.object.isRequired,
'proxyInfo': React.PropTypes.object.isRequired,
'messageBox': React.PropTypes.object.isRequired,
'filter': React.PropTypes.object /*.isRequired */
'filter': React.PropTypes.object /*.isRequired */,
'attachmentQueueInfo': React.PropTypes.object.isRequired,
},
//----------------------------------------------------------------------------
@@ -48,15 +49,31 @@ Clipperz.PM.UI.Components.CardToolbarClass = React.createClass({
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'toggleSettingsPanel');
},
attachmentQueueToggleHandler: function(anEvent) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'toggleAttachmentQueueBox');
},
//============================================================================
renderWithSidePanels: function () {
var attachmentDownloadNotificationNumber = MochiKit.Base.filter(function(anElement) {
return anElement['queueElement']['process'] == 'DOWNLOAD';
}, this.props['attachmentQueueInfo']['notifications']).length;
var attachmentUploadNotificationNumber = this.props['attachmentQueueInfo']['notifications'].length - attachmentDownloadNotificationNumber;
return [
React.DOM.div({className:'selectionToggle'}, [
Clipperz.PM.UI.Components.Button({eventName:'selectionToggleButton', label:"tags", handler:this.selectionToggleHandler})
]),
this.renderWithoutSidePanels(),
// React.DOM.div({className:'attachmentToggle'}, [
// Clipperz.PM.UI.Components.Button({eventName:'attachmentQueueToggleButton', label:"clipperz", handler:this.attachmentQueueToggleHandler})
// ]),
// TODO: validate and adjust names
React.DOM.div({className:'settingsToggle'}, [
Clipperz.PM.UI.Components.Button({eventName:'attachmentQueueToggleButton', label:"\u2191\u2193", handler:this.attachmentQueueToggleHandler, badgeTopContent: attachmentDownloadNotificationNumber, badgeBottomContent: attachmentUploadNotificationNumber}),
Clipperz.PM.UI.Components.Button({eventName:'settingsToggleButton', label:"menu", handler:this.settingsToggleHandler})
])
];
@@ -70,6 +87,8 @@ Clipperz.PM.UI.Components.CardToolbarClass = React.createClass({
if (this.props['filter']['type'] == 'RECENT') {
result = [React.DOM.div({className:'clipperz'}, [React.DOM.span({className:'logo recent'}, "recent")])];
} else if (this.props['filter']['type'] == 'WITH_ATTACHMENTS') {
result = [React.DOM.div({className:'clipperz'}, [React.DOM.span({className:'logo withAttachments'}, "attachment")])];
} else if (this.props['filter']['type'] == 'TAG') {
result = [React.DOM.div({className:'clipperz'}, [
React.DOM.span({className:'logo tag'}, "tag"),

View File

@@ -136,6 +136,7 @@ Clipperz.PM.UI.Components.Cards.CommandToolbarClass = React.createClass({
var classes = {
'cardDetailToolbar': true,
'commands': true,
'top': true,
};
classes[style] = true;

View File

@@ -40,6 +40,7 @@ Clipperz.PM.UI.Components.Cards.DetailClass = React.createClass({
result['showGlobalMask'] = this.props['showGlobalMask'];
result['allTags'] = this.props['allTags'];
result['preferences'] = this.props['preferences'];
result['attachmentQueueInfo'] = this.props['attachmentQueueInfo'];
}
return result;

View File

@@ -41,6 +41,8 @@ Clipperz.PM.UI.Components.Cards.EditClass = React.createClass({
'fromFieldPosition': -1,
'toFieldPosition': -1,
'dropPosition': -1,
'skippedFiles': [],
};
},
@@ -581,7 +583,14 @@ console.log("DROP"); //, anEvent);
},
renderAddNewField: function () {
return React.DOM.div({'className':'newCardField', 'onClick':this.addNewField}, "add new field");
// return React.DOM.div({'className':'newCardField', 'onClick':this.addNewField}, "add new field");
return React.DOM.div({'className':'newCardField', 'onClick':this.addNewField}, [
React.DOM.div({'className':'fieldGhostShadow'}, [
React.DOM.div({'className':'label'}, ""),
React.DOM.div({'className':'value'}, ""),
]),
React.DOM.div({'className':'addNewFieldButton'}, "add new field"),
]);
},
//............................................................................
@@ -603,6 +612,210 @@ console.log("DROP"); //, anEvent);
//............................................................................
handleRemoveAttachment: function(anAttachment) {
// MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'cancelAttachment', anAttachment);
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'removeAttachment', this.record(), anAttachment);
},
//............................................................................
uploadFiles: function(someFiles) {
var i;
var newSkippedFiles = [];
for (i = 0; i < someFiles.length; i++) {
var file = someFiles[i];
if (file.size <= Clipperz.PM.DataModel.Attachment.MAX_ATTACHMENT_SIZE) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'addAttachment', this.record(), file);
} else {
newSkippedFiles.push(file);
this.setState({'skippedFiles': newSkippedFiles});
}
}
// TODO: check compatibility with all browsers
this.refs['attachmentInput'].getDOMNode().value = null;
},
//............................................................................
handleFileSelect: function(anEvent) {
this.uploadFiles(anEvent.target.files);
},
//............................................................................
handleOnDrop: function (anEvent) {
anEvent.preventDefault();
this.uploadFiles(anEvent.dataTransfer.files);
},
handleOnDragOver: function (anEvent) {
// Somehow necessary:
// http://enome.github.io/javascript/2014/03/24/drag-and-drop-with-react-js.html
// https://code.google.com/p/chromium/issues/detail?id=168387
// http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html
anEvent.preventDefault();
},
//............................................................................
renderSkippedFiles: function() {
var result;
result = null;
if (this.state['skippedFiles'].length > 0) {
result = React.DOM.div({'className': 'skippedFiles'},[
React.DOM.p({}, "The following files exceed the size limit of " + filesize(Clipperz.PM.DataModel.Attachment.MAX_ATTACHMENT_SIZE)),
React.DOM.ul({},
MochiKit.Base.map(function(aFile) {
return React.DOM.li({}, [
React.DOM.span({'className': 'filename'}, aFile.name),
React.DOM.span({}, " (" + filesize(aFile.size) + ")"),
]);
}, this.state['skippedFiles'])
),
]);
}
return result;
},
renderAttachmentProgress: function(aStatus, aServerStatus, aProgress) {
var result;
var broken = (! aServerStatus && (! aStatus || aStatus == 'CANCELED' || aStatus == 'FAILED' || aStatus == 'DONE'));
result = null;
if (aStatus == 'UPLOADING' || aStatus == 'DOWNLOADING') {
result = Clipperz.PM.UI.Components.RadialProgressIndicator({
'progress': aProgress,
'border': 1
});
} else if (! broken && aStatus != 'DONE' && aStatus != 'FAILED' && aServerStatus != 'AVAILABLE') {
result = Clipperz.PM.UI.Components.RadialProgressIndicator({
'progress': 0,
'border': 1,
'additionalClasses': ['waiting'],
});
}
return result;
},
renderAttachmentStatus: function(aStatus, aServerStatus, aProgress) {
var result;
var status = aStatus ? aStatus : false;
result = null;
if (status == 'FAILED') {
result = React.DOM.span({'className': 'failed'}, "failed");
} else if (status == 'UPLOADING' || status == 'DOWNLOADING') {
var actionSymbol = (status == 'UPLOADING') ? "\u2b06" : "\u2b07";
result = React.DOM.span({'className': 'progressStatus'}, actionSymbol + Math.floor(aProgress*100) + '%');
} else if (aServerStatus != 'AVAILABLE') {
switch(status) {
case 'CANCELED':
result = React.DOM.span({'className': 'broken'}, "canceled");
break;
case 'DONE':
result = React.DOM.span({'className': 'broken'}, "failed");
break;
case false:
result = React.DOM.span({'className': 'broken'}, "failed");
break;
default:
result = React.DOM.span({'className': 'waiting'}, "waiting");
}
}
return result;
},
renderAttachmentActions: function(aStatus, aServerStatus, anAttachment) {
var result;
result = null;
if (aStatus != 'DOWNLOADING') {
result = React.DOM.a({
'className': 'remove',
'onClick': MochiKit.Base.method(this, 'handleRemoveAttachment', anAttachment),
}, "remove field");
}
return result;
},
renderAttachment: function(anAttachment) {
var queueInfo = this.props['attachmentQueueInfo'].elementFetchCallback(anAttachment._reference) || [];
var queueStatus = queueInfo['status'];
var serverStatus = this.props['attachmentServerStatus'][anAttachment._reference];
var broken = (! serverStatus && (! queueStatus || queueStatus == 'CANCELED' || queueStatus == 'FAILED' || queueStatus == 'DONE'));
// console.log(anAttachment['name'], queueStatus)
var status = this.renderAttachmentStatus(queueStatus, serverStatus, queueInfo['requestProgress']);
var actions = this.renderAttachmentActions(queueStatus, serverStatus, anAttachment['_attachment']);
var progressIndicator = this.renderAttachmentProgress(queueStatus, serverStatus, queueInfo['requestProgress']);;
return React.DOM.li({
'className': (broken) ? 'broken' : '',
'key': anAttachment._reference
}, [
React.DOM.span({'className': 'contentType'}, Clipperz.PM.DataModel.Attachment.contentTypeIcon(anAttachment.contentType)),
React.DOM.span({'className': 'meta'}, [
React.DOM.span({'className': 'name'}, anAttachment.name),
React.DOM.span({'className': 'size'}, filesize(anAttachment.size)),
]),
React.DOM.span({'className': 'status'}, status),
React.DOM.span({'className': 'progress'}, progressIndicator),
React.DOM.span({'className': 'actions'}, actions),
])
},
renderAttachments: function(someAttachments) {
return React.DOM.div({'className':'cardAttachmentWrapper'}, [
React.DOM.div({'className': 'cardAttachments'}, [
React.DOM.h3({'className': 'summaryText'}, "Attachments"),
// React.DOM.p({'className': 'summaryText'}, someAttachments.length + ' files attached'),
this.renderSkippedFiles(),
React.DOM.ul({'className': 'attachmentList'},
MochiKit.Base.map(MochiKit.Base.method(this, 'renderAttachment'), someAttachments)
)
]),
React.DOM.div({
'className': 'cardUploadAttachments',
'onClick': MochiKit.Base.bind(function() { this.refs['attachmentInput'].getDOMNode().click() }, this),
'onDragOver': this.handleOnDragOver,
'onDrop': this.handleOnDrop,
},[
React.DOM.p({}, "Drag and drop your files here"),
React.DOM.p({}, "or"),
React.DOM.input({
'type': 'file',
'id': 'attachmentInput',
'className': 'attachmentInput',
'name': 'attachmentInput',
'ref': 'attachmentInput',
'onChange': this.handleFileSelect,
'multiple': true
}),
React.DOM.a({
'className': 'button',
'onDragOver': this.handleOnDragOver,
'onDrop': this.handleOnDrop,
}, "select files"),
])
])
},
//............................................................................
render: function () {
var classes = {
'edit': true
@@ -618,8 +831,9 @@ console.log("DROP"); //, anEvent);
this.renderTags(this.props['tags']),
this.renderFields(this.fields()),
this.renderAddNewField(),
this.renderAttachments(MochiKit.Base.values(this.props['attachments'])),
this.renderNotes(this.props['notes']),
this.renderDirectLogins(this.props['directLogins'])
this.renderDirectLogins(this.props['directLogins']),
])
]),
this.props['ask'] ? Clipperz.PM.UI.Components.DialogBox(this.props['ask']) : null

View File

@@ -63,6 +63,7 @@ Clipperz.PM.UI.Components.Cards.ListClass = React.createClass({
result = React.DOM.li({'className':Clipperz.PM.UI.Components.classNames(classes), 'onClick': this.handleClick, 'key':anItem['_reference'], 'data-reference':anItem['_reference'], 'data-label':anItem['label']}, [
// React.DOM.span({'className':'favicon'}, Clipperz.PM.UI.Components.Cards.FavIcon({'src':anItem['favicon']})),
React.DOM.span({'className':'label'}, anItem['label']),
React.DOM.span({'className':'attachmentsCount'}, anItem['attachmentsCount'] ? 'attachment' : ''),
]);
}

View File

@@ -130,7 +130,7 @@ Clipperz.PM.UI.Components.Cards.TagEditorClass = React.createClass({
renderEditField: function () {
return [
React.DOM.input({'type':'text', 'list':'tagListData', 'onKeyDown':this.handleKeyDown, 'onBlur':this.handleBlur, 'placeholder': "tag"}),
// React.DOM.input({'type':'text', 'list':'tagListData', 'onKeyDown':this.handleKeyDown, 'onBlur':this.handleBlur, 'placeholder': "tag"}),
React.DOM.datalist({'id':'tagListData'}, MochiKit.Base.map(function (aTag) { return React.DOM.option({}, aTag); }, this.listOfTagsNotUsedYet()))
];
},
@@ -145,6 +145,7 @@ Clipperz.PM.UI.Components.Cards.TagEditorClass = React.createClass({
return React.DOM.div({'className':Clipperz.PM.UI.Components.classNames(classes)}, [
React.DOM.ul({},[
MochiKit.Base.map(this.renderTag, this.props['selectedTags']),
this.isReadOnly() ? null : React.DOM.li({}, React.DOM.input({'type':'text', 'list':'tagListData', 'onKeyDown':this.handleKeyDown, 'onBlur':this.handleBlur, 'placeholder': "tag"})),
]),
this.isReadOnly() ? null : this.renderEditField()
]);

View File

@@ -63,6 +63,7 @@ Clipperz.PM.UI.Components.Cards.TextAreaClass = React.createClass({
handleKeyDown: function (anEvent) {
switch (anEvent.keyCode) {
case 27: // escape
// console.log("ESCAPE");
Mousetrap.trigger('esc');
break;
}
@@ -78,7 +79,7 @@ Clipperz.PM.UI.Components.Cards.TextAreaClass = React.createClass({
recalculateSize_1: function () {
var node = this.getDOMNode();
node.style.height = 'auto';
node.style.height = '33px';
node.style.height = node.scrollHeight + 'px';
window.scrollTo(window.scrollLeft, (node.scrollTop + node.scrollHeight));
},

View File

@@ -116,6 +116,17 @@ Clipperz.PM.UI.Components.Cards.ViewClass = React.createClass({
return result;
},
//----------------------------------------------------------------------------
handleGetAttachment: function (anAttachment) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'getAttachment', anAttachment);
},
handleCancelDownload: function (anAttachment) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'cancelAttachment', anAttachment);
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'closeAttachment', anAttachment);
},
//----------------------------------------------------------------------------
renderEmpty: function () {
@@ -251,6 +262,132 @@ Clipperz.PM.UI.Components.Cards.ViewClass = React.createClass({
//............................................................................
renderAttachmentProgress: function(aStatus, aServerStatus, aProgress) {
var result;
var broken = (! aServerStatus && (! aStatus || aStatus == 'CANCELED' || aStatus == 'FAILED' || aStatus == 'DONE'));
result = null;
if (aStatus == 'UPLOADING' || aStatus == 'DOWNLOADING') {
result = Clipperz.PM.UI.Components.RadialProgressIndicator({
'progress': aProgress,
'border': 1
});
} else if (! broken && aStatus != 'DONE' && aServerStatus != 'AVAILABLE') {
result = Clipperz.PM.UI.Components.RadialProgressIndicator({
'progress': 0,
'border': 1,
'additionalClasses': ['waiting'],
});
}
return result;
},
renderAttachmentStatus: function(aStatus, aServerStatus, aProgress) {
var result;
var status = aStatus ? aStatus : false;
result = null;
if (status == 'FAILED') {
result = React.DOM.span({'className': 'failed'}, "failed");
} else if (status == 'UPLOADING' || status == 'DOWNLOADING') {
var actionSymbol = (status == 'UPLOADING') ? "\u2b06" : "\u2b07";
result = React.DOM.span({'className': 'progressStatus'}, actionSymbol + Math.floor(aProgress*100) + '%');
} else if (aServerStatus != 'AVAILABLE') {
switch(status) {
case 'CANCELED':
result = React.DOM.span({'className': 'broken'}, "canceled");
break;
case 'DONE':
result = React.DOM.span({'className': 'broken'}, "failed");
break;
case false:
result = React.DOM.span({'className': 'broken'}, "failed");
break;
default:
result = React.DOM.span({'className': 'waiting'}, "waiting");
}
}
return result;
},
renderAttachmentActions: function(aStatus, aServerStatus, anAttachment) {
var result;
result = null;
if (aStatus == 'DOWNLOADING') {
result = React.DOM.a({
'className': 'cancel',
'onClick': MochiKit.Base.method(this, 'handleCancelDownload', anAttachment)
}, "remove field");
} else if (aServerStatus == 'AVAILABLE') {
result = React.DOM.a({
'className': 'download',
'onClick': MochiKit.Base.method(this, 'handleGetAttachment', anAttachment),
}, "\u2b07");
}
return result;
},
renderAttachment: function (anAttachment) {
var result;
if (this.props['attachmentQueueInfo'].elementFetchCallback != null) {
var queueInfo = this.props['attachmentQueueInfo'].elementFetchCallback(anAttachment._reference) || [];
var queueStatus = queueInfo['status'];
var serverStatus = this.props['attachmentServerStatus'][anAttachment._reference];
var broken = (! serverStatus && (! queueStatus || queueStatus == 'CANCELED'));
var status = this.renderAttachmentStatus(queueStatus, serverStatus, queueInfo['requestProgress']);
var actions = this.renderAttachmentActions(queueStatus, serverStatus, anAttachment['_attachment']);
var progressIndicator = this.renderAttachmentProgress(queueStatus, serverStatus, queueInfo['requestProgress']);
result = React.DOM.li({
'className': (broken) ? 'broken' : '',
'key': anAttachment._reference
}, [
React.DOM.span({'className': 'contentType'}, Clipperz.PM.DataModel.Attachment.contentTypeIcon(anAttachment.contentType)),
React.DOM.span({'className': 'meta'}, [
React.DOM.span({'className': 'name'}, anAttachment.name),
React.DOM.span({'className': 'size'}, filesize(anAttachment.size)),
]),
React.DOM.span({'className': 'status'}, status),
React.DOM.span({'className': 'progress'}, progressIndicator),
React.DOM.span({'className': 'actions'}, actions),
])
} else {
result = null;
}
return result;
},
renderAttachments: function(someAttachments) {
var result;
//console.log("View props:", this.props, someAttachments);
if (someAttachments.length > 0) {
result = React.DOM.div({'className': 'cardAttachments'}, [
React.DOM.h3({'className': 'summaryText'}, "Attachments"),
// React.DOM.p({'className': 'summaryText'}, someAttachments.length + ' files attached'),
React.DOM.ul({'className': 'attachmentList'},
MochiKit.Base.map(MochiKit.Base.method(this, 'renderAttachment'), someAttachments)
)
]);
} else {
result = [];
}
return result;
},
//............................................................................
renderCard: function () {
var classes = {
'view': true,
@@ -263,8 +400,9 @@ Clipperz.PM.UI.Components.Cards.ViewClass = React.createClass({
this.renderLabel(this.props['label']),
this.renderTags(this.props['tags']),
this.renderFields(this.props['fields']),
this.renderAttachments(MochiKit.Base.values(this.props['attachments'])),
this.renderNotes(this.props['notes']),
this.renderDirectLogins(this.props['directLogins'])
this.renderDirectLogins(this.props['directLogins']),
]),
this.props['ask'] ? Clipperz.PM.UI.Components.DialogBox(this.props['ask']) : null
]);

View File

@@ -0,0 +1,139 @@
/*
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/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components');
Clipperz.PM.UI.Components.DocumentEncryptionSandboxClass = React.createClass({
getInitialState: function() {
return {
'documentMeta': null,
'encryptedDocument': null,
};
},
//=========================================================================
// Event Handlers
//=========================================================================
handleFileSelect: function(anEvent) {
// console.log("handleFileSelect: files:", anEvent.target.files);
var fileToRead = anEvent.target.files[0];
// console.log(fileToRead);
// return;
if (fileToRead) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'uploadNewFile', null, fileToRead);
}
},
//=========================================================================
// File Handling Methods
//=========================================================================
// handleEncryptionSynchronously: function(aKey, aByteArray) {
// var encryptedDocument;
// var startTime, endTime;
// console.log("Encrypting...");
// startTime = Date.now()
// encryptedDocument = Clipperz.Crypto.AES.encrypt(aKey, aByteArray);
// endTime = Date.now();
// console.log("Finished! Encryption took", (endTime-startTime)/1000, "seconds.");
// this.setState({
// 'encryptedDocument': encryptedDocument,
// });
// },
downloadFile: function(aFileReference) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadFile', aFileReference);
},
//=========================================================================
// Render Methods
//=========================================================================
renderUploadButton: function() {
var result;
if (window.File && window.FileReader && window.FileList && window.Blob) {
result = React.DOM.input({
'type': 'file',
'id': 'files',
'name': 'files',
'onChange': this.handleFileSelect,
'multiple': false
})
} else {
result = React.DOM.p({}, "Browser doesn't support the FileReader API");
}
return result;
},
renderQueue: function() {
return MochiKit.Base.map(MochiKit.Base.bind(function(anElement) {
return React.DOM.li({},[
React.DOM.ul({}, [
React.DOM.li({}, anElement['meta']['name']),
React.DOM.li({}, anElement['meta']['size']),
React.DOM.li({}, anElement['status']),
React.DOM.li({}, Math.floor((anElement['currentByte']/anElement['meta']['size'])*100)+'%'),
React.DOM.li({}, React.DOM.button({
'disabled': ! anElement.isDataReady,
'onClick': MochiKit.Base.method(this, 'downloadFile', anElement['reference']),
}, "Download File")),
])
]);
}, this), this.props['attachmentQueueInfo']);
},
render: function () {
return React.DOM.div({
'style': {
'position': 'fixed',
'width': '80%',
'height': '80%',
'background': 'white',
'color': 'black',
'top': '10%',
'left': '10%',
'border': '2px solid black',
'zIndex': '10'
}
}, [
React.DOM.p({}, "This is the doc encryption proof of concept."),
React.DOM.p({}, "*Status*: working streaming encryption and decryption! Current size limit is 250MB"),
this.renderUploadButton(),
React.DOM.ul({},this.renderQueue()),
]);
}
//=========================================================================
});
Clipperz.PM.UI.Components.DocumentEncryptionSandbox = React.createFactory(Clipperz.PM.UI.Components.DocumentEncryptionSandboxClass);

View File

@@ -135,10 +135,10 @@ Clipperz.PM.UI.Components.ExtraFeatures.OTPClass = React.createClass({
'</html>'
);
newWindow.document.close();
newWindow.focus();
newWindow.print();
newWindow.close();
// newWindow.document.close();
// newWindow.focus();
// newWindow.print();
// newWindow.close();
},
//=========================================================================

View File

@@ -34,17 +34,18 @@ Clipperz.PM.UI.Components.Pages.MainPageClass = React.createClass({
},
propTypes: {
'tags': React.PropTypes.object,
'allTags': React.PropTypes.array,
'messageBox': React.PropTypes.object.isRequired,
'featureSet': React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
'features': React.PropTypes.array.isRequired,
'userInfo': React.PropTypes.object.isRequired,
'accountInfo': React.PropTypes.object.isRequired,
'style': React.PropTypes.oneOf(Clipperz_PM_UI_availableStyles).isRequired,
'locked': React.PropTypes.bool,
// 'mediaQueryStyle': React.PropTypes.oneOf(['extra-short', 'narrow', 'wide', 'extra-wide']).isRequired,
// 'cards': React.PropTypes.deferred.isRequired
'tags': React.PropTypes.object,
'allTags': React.PropTypes.array,
'messageBox': React.PropTypes.object.isRequired,
'featureSet': React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
'features': React.PropTypes.array.isRequired,
'userInfo': React.PropTypes.object.isRequired,
'accountInfo': React.PropTypes.object.isRequired,
'style': React.PropTypes.oneOf(Clipperz_PM_UI_availableStyles).isRequired,
'locked': React.PropTypes.bool,
'attachmentQueueInfo': React.PropTypes.object.isRequired,
// 'mediaQueryStyle': React.PropTypes.oneOf(['extra-short', 'narrow', 'wide', 'extra-wide']).isRequired,
// 'cards': React.PropTypes.deferred.isRequired
},
getInitialState: function () {
@@ -67,10 +68,11 @@ Clipperz.PM.UI.Components.Pages.MainPageClass = React.createClass({
classes[this.props['style']] = true;
result = React.DOM.div({'key':'mainPage', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [
Clipperz.PM.UI.Components.AttachmentQueueBox(this.props),
this.props['style'] != 'extra-wide' ? Clipperz.PM.UI.Components.Panels.SelectionPanel(this.props) : null,
Clipperz.PM.UI.Components.Panels.MainPanel(this.props),
Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanel(this.props),
this.props['ask'] ? Clipperz.PM.UI.Components.DialogBox(this.props['ask']) : null
this.props['ask'] ? Clipperz.PM.UI.Components.DialogBox(this.props['ask']) : null,
]);
}

View File

@@ -29,11 +29,12 @@ Clipperz.PM.UI.Components.Panels.MainPanelClass = React.createClass({
//=========================================================================
propTypes: {
'allTags': React.PropTypes.array,
'messageBox': React.PropTypes.object.isRequired,
'featureSet': React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
'features': React.PropTypes.array.isRequired,
'style': React.PropTypes.oneOf(Clipperz_PM_UI_availableStyles).isRequired,
'allTags': React.PropTypes.array,
'messageBox': React.PropTypes.object.isRequired,
'featureSet': React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
'features': React.PropTypes.array.isRequired,
'style': React.PropTypes.oneOf(Clipperz_PM_UI_availableStyles).isRequired,
'attachmentQueueInfo': React.PropTypes.object.isRequired,
},
style: function () {

View File

@@ -0,0 +1,117 @@
/*
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/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components');
Clipperz.PM.UI.Components.RadialProgressIndicatorClass = React.createClass({
propTypes: {
'progress': React.PropTypes.number.isRequired,
},
getInitialState: function() {
return {};
},
//=========================================================================
getPathDefinition: function(aRadius, aProgress) {
aProgress = (aProgress <= 1) ? aProgress : 1;
aProgress = (aProgress >= 0) ? aProgress : 0;
var pi = Math.PI;
var radiantAngle = 2 * pi * aProgress;
var x = Math.sin( radiantAngle ) * aRadius;
var y = Math.cos( radiantAngle ) * - aRadius;
var mid = (aProgress > 0.5) ? 1 : 0;
return 'M 0 0 ' + // Start from origin
'v ' + (-aRadius) +' ' + // Straight vertical up
'A ' + aRadius + ' ' + aRadius + ' 1 ' + // Arc, vertical radius, horizontal radius, xAxisRotate (?)...
mid + ' 1 ' + // ...lrge arc flag,
x + ' ' + // ...destination x
y + ' z'; // ...destination y, close path
//[x,y].forEach(function( d ){
// d = Math.round( d * 1e3 ) / 1e3;
//});
},
//=========================================================================
getAdditionalClassesString(aList) {
var result;
aList = aList || [];
result = aList.join(' ');
return ' '+result;
},
render: function () {
var border;
var additionalClasses = this.getAdditionalClassesString(this.props['additionalClasses']);
var radius = Clipperz.PM.UI.Components.DEFAULT_RADIUS;
var borderSize = (this.props['border']) ? this.props['border'] : 0;
var boxSize = 2 * (radius + borderSize) + 2;
var center = boxSize/2;
border = null;
if (this.props['border']) {
border = React.DOM.circle({
'className': 'border',
'cx': center,
'cy': center,
'r': radius+this.props['border'],
});
}
return React.DOM.svg({
'className': 'radialProgressIndicator' + additionalClasses,
'viewBox': '0 0 ' + boxSize + ' ' + boxSize,
}, [
border,
React.DOM.circle({
'className': 'background',
'cx': center,
'cy': center,
'r': radius,
}),
React.DOM.path({
'className': 'progress',
'transform': 'translate(' + (center) + ', ' + (center) + ')',
'd': this.getPathDefinition(radius, this.props['progress'])
})
])
}
//=========================================================================
});
Clipperz.PM.UI.Components.RadialProgressIndicator = React.createFactory(Clipperz.PM.UI.Components.RadialProgressIndicatorClass);
Clipperz.PM.UI.Components.DEFAULT_RADIUS = 10; // You can resize the SVG object with CSS anyway

View File

@@ -36,6 +36,10 @@ Clipperz.PM.UI.Components.SelectionsClass = React.createClass({
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'selectRecentCards');
},
selectWithAttachments: function (anEvent) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'selectCardsWithAttachments');
},
selectUntaggedCards: function (anEvent) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'selectUntaggedCards');
},
@@ -76,6 +80,7 @@ Clipperz.PM.UI.Components.SelectionsClass = React.createClass({
var filterValue;
//console.log("SELECTIONS PROPS", this.props);
//console.log("withAttachmentCardsCount", this.props['withAttachmentCardsCount']);
tagInfo = this.props['tags'] ? this.props['tags'] : {};
tags = MochiKit.Base.filter(Clipperz.PM.DataModel.Record.isRegularTag, MochiKit.Base.keys(tagInfo)).sort(Clipperz.Base.caseInsensitiveCompare);
archivedCardsCount = this.props['archivedCardsCount'];
@@ -94,7 +99,10 @@ Clipperz.PM.UI.Components.SelectionsClass = React.createClass({
React.DOM.span({'className':'label'}, "Recent"),
React.DOM.span({'className':'count'}, this.props['allCardsCount'] ? '10' : '-')
]),
// React.DOM.li({'className':'untaggedCards', 'onClick': this.selectUntaggedCards}, "Untagged - " + this.props['untaggedCardsCount'])
React.DOM.li({'className':'withAttachmentCards', 'onClick': this.selectWithAttachments}, [
React.DOM.span({'className':'label'}, "With attachments"),
React.DOM.span({'className':'count'}, this.props['withAttachmentCardsCount'] ? this.props['withAttachmentCardsCount'] : '-')
]),
React.DOM.li({'className':'untaggedCards', 'onClick': this.selectUntaggedCards}, [
React.DOM.span({'className':'label'}, "Untagged"),
React.DOM.span({'className':'count'}, this.props['untaggedCardsCount'] ? this.props['untaggedCardsCount'] : '-')

View File

@@ -35,6 +35,7 @@ Clipperz.PM.UI.MainController = function() {
this._isSelectionPanelOpen = false;
this._isSettingsPanelOpen = false;
this._isAttachmentQueueBoxOpen = false;
this._pageStack = ['loadingPage'];
this._overlay = new Clipperz.PM.UI.Components.Overlay();
@@ -75,9 +76,9 @@ Clipperz.PM.UI.MainController = function() {
'importCards',
'downloadExport',
'updateProgress',
'toggleSelectionPanel', 'hideSelectionPanel', 'toggleSettingsPanel',
'toggleSelectionPanel', 'hideSelectionPanel', 'toggleSettingsPanel', 'toggleAttachmentQueueBox',
'matchMediaQuery', 'unmatchMediaQuery',
'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards',
'selectAllCards', 'selectRecentCards', 'selectCardsWithAttachments', 'selectUntaggedCards', 'tagSelected', 'search',
'refreshCardEditDetail',
'saveCardEdits', 'cancelCardEdits',
'selectCard',
@@ -90,9 +91,16 @@ Clipperz.PM.UI.MainController = function() {
'closeHelp',
'downloadOfflineCopy',
'runDirectLogin', 'removeDirectLogin',
'exitSearch'
'exitSearch',
'updateAttachmentQueueInfo', 'addAttachment', 'removeAttachment', 'getAttachment', 'cancelAttachment', 'closeAttachmentNotification',
]);
this._attachmentController = new Clipperz.PM.UI.AttachmentController({
'uploadMessageCallback': MochiKit.Base.method(this, 'uploadMessageCallback'),
'downloadMessageCallback': MochiKit.Base.method(this, 'downloadMessageCallback'),
'reloadServerStatusCallback': MochiKit.Base.method(this, 'reloadAttachmentServerStatusCallback')
});
Mousetrap.bind(['/'], MochiKit.Base.method(this, 'focusOnSearch'));
Mousetrap.bind(['left', 'h', 'esc'], MochiKit.Base.method(this, 'exitCurrentSelection'));
@@ -205,7 +213,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
renderPages: function (pages) {
var self = this;
MochiKit.Iter.forEach(pages, function (aPageName) {
//console.log("RENDERING", aPageName);
self._pages[aPageName] = React.render(
Clipperz.PM.UI.Components.Pages[self.capitaliseFirstLetter(aPageName)](self.pageProperties(aPageName)),
MochiKit.DOM.getElement(aPageName)
@@ -437,7 +444,7 @@ Clipperz.log("THE BROWSER IS OFFLINE");
getPassphraseDelegate = MochiKit.Base.partial(MochiKit.Async.succeed, passphrase);
user = new Clipperz.PM.DataModel.User({'username':oldUser.username(), 'getPassphraseFunction':getPassphraseDelegate});
deferredResult = new Clipperz.Async.Deferred('MainController.unlock_handler', {trace:true});
deferredResult = new Clipperz.Async.Deferred('MainController.unlock_handler', {trace:false});
deferredResult.addMethod(unlockPage, 'setProps', {'disabled': true});
@@ -539,7 +546,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
},
setUser: function (aUser) {
//console.log("SET USER", aUser);
this._user = aUser;
return this._user;
},
@@ -604,11 +610,30 @@ Clipperz.log("THE BROWSER IS OFFLINE");
return deferredResult;
},
collectAttachmentInfo: function(anAttachment) {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred('MainController.collectAttachmentInfo', {trace:false});
deferredResult.setValue('_attachment'); // The object itself, maybe this should be the only value being passed
deferredResult.addMethod(anAttachment, 'reference');
deferredResult.setValue('_reference');
deferredResult.addMethod(anAttachment, 'name');
deferredResult.setValue('name');
deferredResult.addMethod(anAttachment, 'contentType');
deferredResult.setValue('contentType');
deferredResult.addMethod(anAttachment, 'size');
deferredResult.setValue('size');
deferredResult.values();
deferredResult.callback(anAttachment);
return deferredResult;
},
collectRecordInfo: function (aRecord) {
var deferredResult;
//console.log("collectRecordInfo");
deferredResult = new Clipperz.Async.Deferred('MainController.collectRecordInfo', {trace:false});
deferredResult.setValue('_record');
deferredResult.addMethod(aRecord, 'reference');
@@ -631,12 +656,21 @@ Clipperz.log("THE BROWSER IS OFFLINE");
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.setValue('fields');
deferredResult.addMethod(aRecord, 'attachments');
deferredResult.addCallback(MochiKit.Base.values);
deferredResult.addCallback(MochiKit.Base.map, MochiKit.Base.method(this, 'collectAttachmentInfo'));
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.setValue('attachments');
deferredResult.addMethod(aRecord, 'directLogins');
deferredResult.addCallback(MochiKit.Base.values);
deferredResult.addCallback(MochiKit.Base.map, MochiKit.Base.method(this, 'collectDirectLoginInfo'));
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.setValue('directLogins');
deferredResult.addMethod(aRecord, 'getAttachmentServerStatus');
deferredResult.setValue('attachmentServerStatus');
deferredResult.values();
deferredResult.callback(aRecord);
@@ -668,8 +702,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
deferredResult = new Clipperz.Async.Deferred('MainController.updateSelectedCard', {trace:false});
deferredResult.addMethod(this.user(), 'getRecord', someInfo['reference']);
// deferredResult.addMethod(this, function(d) {console.log(d); return d;});
deferredResult.addMethod(this, 'collectRecordInfo');
deferredResult.addMethod(this, 'setPageProperties', 'mainPage', 'selectedCard');
if ((this.mediaQueryStyle() == 'narrow') && shouldShowCardDetail) {
@@ -774,6 +806,10 @@ Clipperz.log("THE BROWSER IS OFFLINE");
filterCriteria = MochiKit.Base.operator.truth;
sortCriteria = Clipperz.Base.reverseComparator(MochiKit.Base.keyComparator('_accessDate'));
rangeFilter = function (someCards) { return someCards.slice(0, 10)};
} else if (aFilter['type'] == 'WITH_ATTACHMENTS') {
filterCriteria = function (aRecordInfo) { return aRecordInfo.attachmentsCount > 0; };
sortCriteria = Clipperz.Base.caseInsensitiveKeyComparator('label');
rangeFilter = MochiKit.Base.operator.identity;
} else if (aFilter['type'] == 'SEARCH') {
filterCriteria = this.regExpFilterGenerator(Clipperz.PM.DataModel.Record.regExpForSearch(aFilter['value']));
sortCriteria = Clipperz.Base.caseInsensitiveKeyComparator('label');
@@ -879,7 +915,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
MochiKit.Base.method(this.user(), 'getPreference', 'lock'),
MochiKit.Base.bind(function (someLockInfo) {
if (this._lockTimeout) {
// console.log("clearing previous lock timer");
clearTimeout(this._lockTimeout);
}
@@ -902,7 +937,7 @@ Clipperz.log("THE BROWSER IS OFFLINE");
? MochiKit.Async.succeed
: MochiKit.Base.partial(MochiKit.Base.filter, function (someRecordInfo) { return ! someRecordInfo['_isArchived']; });
return Clipperz.Async.callbacks("MainController.getUntaggedCardsCount", [
return Clipperz.Async.callbacks("MainController.getAllCardsCount", [
MochiKit.Base.method(this.user(), 'getRecords'),
MochiKit.Base.partial(MochiKit.Base.map, Clipperz.Async.collectResults("collectResults", {'_fullLabel':MochiKit.Base.methodcaller('fullLabel'), '_isArchived':MochiKit.Base.methodcaller('isArchived')}, {trace:false})),
Clipperz.Async.collectAll,
@@ -910,7 +945,23 @@ Clipperz.log("THE BROWSER IS OFFLINE");
function (someCards) { return someCards.length; },
], {trace:false});
},
getCardsWithAttachmentsCount: function () {
var archivedCardsFilter = this.shouldIncludeArchivedCards()
? MochiKit.Async.succeed
: MochiKit.Base.partial(MochiKit.Base.filter, function (someRecordInfo) { return ! someRecordInfo['_isArchived']; });
return Clipperz.Async.callbacks("MainController.getCardsWithAttachmentsCount", [
MochiKit.Base.method(this.user(), 'getRecords'),
MochiKit.Base.partial(MochiKit.Base.map, Clipperz.Async.collectResults("collectResults", {'attachmentsCount':MochiKit.Base.methodcaller('attachmentsCount'), '_isArchived':MochiKit.Base.methodcaller('isArchived')}, {trace:false})),
Clipperz.Async.collectAll,
archivedCardsFilter,
function (aResult) {
return MochiKit.Base.filter(function (aRecordInfo) { return aRecordInfo['attachmentsCount'] > 0; }, aResult).length;
}
], {trace:false});
},
getArchivedCardsCount: function () {
return Clipperz.Async.callbacks("MainController.getArchivedCardsCount", [
MochiKit.Base.method(this.user(), 'getRecords'),
@@ -961,6 +1012,8 @@ Clipperz.log("THE BROWSER IS OFFLINE");
MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'allTags'),
MochiKit.Base.method(this, 'getAllCardsCount'),
MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'allCardsCount'),
MochiKit.Base.method(this, 'getCardsWithAttachmentsCount'),
MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'withAttachmentCardsCount'),
MochiKit.Base.method(this, 'getArchivedCardsCount'),
MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'archivedCardsCount'),
MochiKit.Base.method(this, 'getUntaggedCardsCount'),
@@ -979,9 +1032,12 @@ Clipperz.log("THE BROWSER IS OFFLINE");
//=========================================================================
runApplication: function (anUser) {
window.history.replaceState({selectedCardInfo: null}, "", window.location.toString());
window.history.pushState({selectedCardInfo: null}, "", window.location.toString()); // Hack to support filters undo with no other actions
this.moveInPage(this.currentPage(), 'mainPage');
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'enableLock');
this.resetLockTimeout();
this.registerHistoryHandler();
return this.renderAccountData();
},
@@ -1129,11 +1185,13 @@ Clipperz.log("THE BROWSER IS OFFLINE");
this.slidePage(MochiKit.DOM.getElement(fromPage), MochiKit.DOM.getElement(toPage), 'LEFT');
this.setCurrentPage(toPage);
if (shouldAddItemToHistory) {
window.history.pushState({'fromPage': fromPage, 'toPage': toPage}, "");
} else {
// Skipping push on page change: history needs to be more granular
// New states are pushed on user actions (view card, back to main page, ...)
// if (shouldAddItemToHistory) {
// window.history.pushState({'fromPage': fromPage, 'toPage': toPage}, "");
// } else {
//console.log("Skip HISTORY");
}
// }
} else {
//console.log("No need to move in the same page");
}
@@ -1202,6 +1260,32 @@ Clipperz.log("THE BROWSER IS OFFLINE");
}
},
attachmentController: function () {
return this._attachmentController;
},
attachmentQueueInfo: function() {
var queue;
var notifications;
var elementFetchCallback;
if (this._attachmentController) {
queue = this._attachmentController.getQueueInfo();
notifications = this._attachmentController.getNotificationsInfo();
elementFetchCallback = MochiKit.Base.method(this._attachmentController, 'getQueueElement');
} else {
queue = [];
notifications = [];
elementFetchCallback = null;
}
return {
'queue': queue,
'notifications': notifications,
'elementFetchCallback': elementFetchCallback,
}
},
//-------------------------------------------------------------------------
messageBoxContent: function () {
@@ -1211,7 +1295,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
message = "";
level = 'HIDE';
//console.log("messageBox - this.user()", this.user());
if (this.featureSet() == 'EXPIRED') {
message = "Expired subscription";
level = 'ERROR';
@@ -1261,12 +1344,14 @@ Clipperz.log("THE BROWSER IS OFFLINE");
'messageBox': this.messageBoxContent(),
'userInfo': this.userInfo(),
'accountInfo': this.userAccountInfo(),
'selectionPanelStatus': this.isSelectionPanelOpen() ? 'OPEN' : 'CLOSED',
'settingsPanelStatus': this.isSettingsPanelOpen() ? 'OPEN' : 'CLOSED',
'selectionPanelStatus': this.isSelectionPanelOpen() ? 'OPEN' : 'CLOSED',
'settingsPanelStatus': this.isSettingsPanelOpen() ? 'OPEN' : 'CLOSED',
'attachmentQueueBoxStatus': this.isAttachmentQueueBoxOpen() ? 'OPEN' : 'CLOSED',
'featureSet': this.featureSet(),
'features': this.features(),
'proxyInfo': this.proxyInfo(),
'locked': false
'locked': false,
'attachmentQueueInfo': this.attachmentQueueInfo(),
// 'shouldIncludeArchivedCards': this.shouldIncludeArchivedCards(),
// 'cards': …,
// 'tags': …,
@@ -1274,6 +1359,7 @@ Clipperz.log("THE BROWSER IS OFFLINE");
};
} else if (aPageName == 'cardDetailPage') {
extraProperties = {
'attachmentQueueInfo': this.attachmentQueueInfo(),
};
} else if (aPageName == 'errorPage') {
extraProperties = {
@@ -1367,6 +1453,15 @@ Clipperz.log("THE BROWSER IS OFFLINE");
this.refreshCurrentPage();
},
isAttachmentQueueBoxOpen: function() {
return this._isAttachmentQueueBoxOpen;
},
toggleAttachmentQueueBox_handler: function (anEvent) {
this._isAttachmentQueueBoxOpen = !this._isAttachmentQueueBoxOpen;
this.refreshCurrentPage();
},
//----------------------------------------------------------------------------
selectedCardInfo: function () {
@@ -1487,6 +1582,8 @@ Clipperz.log("THE BROWSER IS OFFLINE");
selectCard_handler: function (someInfo, shouldUpdateCardDetail) {
this.selectCard(someInfo, shouldUpdateCardDetail);
window.history.pushState({selectedCardInfo: someInfo}, "", window.location.toString());
// console.log("pushing state");
},
refreshCardEditDetail_handler: function (aRecordReference) {
@@ -1689,6 +1786,10 @@ Clipperz.log("THE BROWSER IS OFFLINE");
},
cancelCardEdits_handler: function (aRecordReference) {
return this.cancelCardEdits(aRecordReference);
},
cancelCardEdits: function (aRecordReference) {
var currentPage = this.pages()[this.currentPage()];
var self = this;
var wasBrandNew;
@@ -1699,7 +1800,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
function (aValue) { wasBrandNew = aValue },
MochiKit.Base.method(this.user(), 'hasPendingChanges'),
//function (aValue) { console.log("2- USER.hasPendingChanges()", aValue); return aValue; },
Clipperz.Async.deferredIf('HasPendingChanges',[
MochiKit.Base.method(self, 'ask', {
'question': "There are pending changes to your card. Ignore changes?",
@@ -1808,7 +1908,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
cloneCard_handler: function (anEvent) {
var cardInfo;
//console.log("CLONE CARD", anEvent['reference']);
return Clipperz.Async.callbacks("MainController.cloneCard_handler", [
MochiKit.Base.method(this.user(), 'getRecord', anEvent['reference']),
MochiKit.Base.method(this.user(), 'cloneRecord'),
@@ -1827,6 +1926,11 @@ Clipperz.log("THE BROWSER IS OFFLINE");
MochiKit.Base.method(this, 'refreshUI'),
], {trace:false});
},
isPageInEditMode: function() {
var currentPage = this.pages()[this.currentPage()];
return currentPage.props['mode'] == 'edit';
},
enterEditMode: function () {
var currentPage = this.pages()[this.currentPage()];
@@ -1844,8 +1948,6 @@ Clipperz.log("THE BROWSER IS OFFLINE");
},
editCard_handler: function (anEvent) {
//console.log("EDIT CARD", anEvent['reference']);
// this.pages()[this.currentPage()].setProps({'mode': 'edit'});
this.enterEditMode();
},
@@ -1881,11 +1983,16 @@ Clipperz.log("THE BROWSER IS OFFLINE");
goBackToMainPage_handler: function (anEvent) {
this.goBackToMainPage(anEvent);
window.history.pushState({selectedCardInfo: null}, "", window.location.toString());
},
//============================================================================
selectAllCards_handler: function () {
this.selectAllCards();
},
selectAllCards: function () {
this.setPageProperties('mainPage', 'searchTerm', '');
this.resetSelectedCard();
this.setFilter('ALL');
@@ -1900,6 +2007,13 @@ Clipperz.log("THE BROWSER IS OFFLINE");
return this.refreshSelectedCards();
},
selectCardsWithAttachments_handler: function () {
this.resetSelectedCard();
this.setFilter('WITH_ATTACHMENTS');
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'hideSelectionPanel');
return this.refreshSelectedCards();
},
search_handler: function (aValue) {
this.resetSelectedCard();
@@ -2044,7 +2158,7 @@ Clipperz.log("THE BROWSER IS OFFLINE");
prevCardInfo = this.previousCardInfo();
shouldUpdateCardDetail = this.shouldShowCardDetailWhenMovingBetweenCardsUsingKeys();
//console.log("PREV CARD INFO", prevCardInfo);
if (prevCardInfo != null) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'selectCard', prevCardInfo, shouldUpdateCardDetail);
}
@@ -2056,7 +2170,7 @@ Clipperz.log("THE BROWSER IS OFFLINE");
nextCardInfo = this.nextCardInfo();
shouldUpdateCardDetail = this.shouldShowCardDetailWhenMovingBetweenCardsUsingKeys();
//console.log("NEXT CARD INFO", shouldUpdateCardDetail);
if (nextCardInfo != null) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'selectCard', nextCardInfo, shouldUpdateCardDetail);
}
@@ -2086,6 +2200,128 @@ Clipperz.log("THE BROWSER IS OFFLINE");
},
//============================================================================
updateAttachmentQueueInfo_handler: function(someProperties) {
this.setPageProperties(this.currentPage(), 'attachmentQueueInfo', this.attachmentQueueInfo());
},
addAttachment_handler: function(aRecord, aFile) { // aReference, someMetadata, aKey, aNonce
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("MainController.addCardAttachment_handler", {'trace':false});
deferredResult.addMethod(aRecord, 'createNewAttachment');
deferredResult.addCallback(MochiKit.Base.methodcaller('setFile', aFile));
deferredResult.addMethod(this.attachmentController(), 'addAttachment');
deferredResult.addCallback(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', aRecord.reference());
deferredResult.callback();
return deferredResult;
},
uploadMessageCallback: function(someArguments, aProgressCallback) {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("MainController.uploadMessageCallback", {'trace':false});
deferredResult.addMethod(this.user().connection(), 'uploadAttachment', someArguments, aProgressCallback);
deferredResult.callback();
return deferredResult;
},
//----------------------------------------------------------------------------
removeAttachment_handler: function(aRecord, anAttachment) {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("MainController.removeAttachment_handler", {trace: false});
deferredResult.addCallback(MochiKit.Base.method(this, 'ask', {
'question': "Do you really want to delete this attachment?",
'possibleAnswers':{
'cancel': {'label':"No", 'isDefault':true, 'answer':MochiKit.Base.methodcaller('cancel', new MochiKit.Async.CancelledError())},
'revert': {'label':"Yes", 'isDefault':false, 'answer':MochiKit.Base.methodcaller('callback')}
}
})),
deferredResult.addMethod(this, 'cancelAttachment_handler', anAttachment);
deferredResult.addMethod(aRecord, 'removeAttachment', anAttachment);
deferredResult.addCallback(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'refreshCardEditDetail', aRecord.reference());
deferredResult.callback();
return deferredResult;
},
//----------------------------------------------------------------------------
getAttachment_handler: function(anAttachment) {
this.attachmentController().getAttachment(anAttachment);
},
downloadMessageCallback: function(anAttachment, aProgressCallback) {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("MainController.downloadAttachment_handler", {'trace':false});
deferredResult.addMethod(this.user().connection(), 'downloadAttachment', {
'reference': anAttachment.reference()
}, aProgressCallback);
deferredResult.callback();
return deferredResult;
},
//----------------------------------------------------------------------------
cancelAttachment_handler: function(anAttachment) {
return this.attachmentController().cancelAttachment(anAttachment);
},
closeAttachmentNotification_handler: function(aNotificationId) {
return this.attachmentController().removeNotification(aNotificationId);
},
//----------------------------------------------------------------------------
reloadAttachmentServerStatusCallback: function(aRecord) {
return Clipperz.Async.callbacks("MainController.reloadAttachmentServerStatus_handler", [
MochiKit.Base.method(this.user(), 'getRecordDetail', aRecord),
MochiKit.Base.bind(function () {
if (this._selectedCardInfo && this._selectedCardInfo['reference']) {
return this.refreshUI(this._selectedCardInfo['reference']);
}
}, this),
], {trace:false});
},
//============================================================================
registerHistoryHandler: function() {
window.onpopstate = MochiKit.Base.method(this, 'handleOnpopstate');
},
handleOnpopstate: function (anEvent) {
if (this.filter().type != 'ALL') {
this.selectAllCards();
window.history.pushState(window.history.state, "", window.location.toString());
} else if(anEvent.state) {
if (this.isPageInEditMode()) {
window.history.pushState({selectedCardInfo: this.selectedCardInfo()}, "", window.location.toString());
this.cancelCardEdits(this.selectedCardReference());
} else {
if (anEvent.state['selectedCardInfo']) {
this.selectCard(anEvent.state['selectedCardInfo'], true);
} else {
this.selectCard(null, true);
this.goBackToMainPage();
}
}
}
// console.log('History changed', anEvent.state);
},
//============================================================================
/*
wrongAppVersion: function (anError) {
// this.pages()['errorPage'].setProps({message:anError.message});