mirror of https://github.com/keeweb/keeweb.git
offline and local storage
This commit is contained in:
parent
4e12537b7a
commit
1e4536a770
2
TODO.md
2
TODO.md
|
@ -13,7 +13,7 @@
|
|||
- [ ] secure fields
|
||||
- [ ] close files
|
||||
- [ ] show sync date
|
||||
- [ ] offline and local storage
|
||||
- [x] offline and local storage
|
||||
- [x] use dropbox chooser for keyfile
|
||||
- [ ] trim history by rules
|
||||
- [ ] advanced search
|
||||
|
|
|
@ -5,6 +5,8 @@ var AppModel = require('./models/app-model'),
|
|||
KeyHandler = require('./comp/key-handler'),
|
||||
Alerts = require('./comp/alerts'),
|
||||
DropboxLink = require('./comp/dropbox-link'),
|
||||
LastOpenFiles = require('./comp/last-open-files'),
|
||||
Storage = require('./comp/storage'),
|
||||
ThemeChanger = require('./util/theme-changer');
|
||||
|
||||
$(function() {
|
||||
|
@ -20,7 +22,7 @@ $(function() {
|
|||
if (appModel.settings.get('theme')) {
|
||||
ThemeChanger.setTheme(appModel.settings.get('theme'));
|
||||
}
|
||||
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0) {
|
||||
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0 && !localStorage.disableSecurityCheck) {
|
||||
Alerts.error({ header: 'Not Secure!', icon: 'user-secret', esc: false, enter: false, click: false,
|
||||
body: 'You have loaded this app with insecure connection. ' +
|
||||
'Someone may be watching you and stealing your passwords. ' +
|
||||
|
@ -37,7 +39,15 @@ $(function() {
|
|||
}
|
||||
|
||||
function showApp() {
|
||||
new AppView({ model: appModel }).render().showOpenFile(appModel.settings.get('lastOpenFile'));
|
||||
var appView = new AppView({ model: appModel }).render();
|
||||
|
||||
var lastOpenFiles = LastOpenFiles.all();
|
||||
var lastOpenFile = lastOpenFiles[0];
|
||||
if (lastOpenFile && lastOpenFile.storage === 'file' && lastOpenFile.path) {
|
||||
appView.showOpenFile(lastOpenFile.path);
|
||||
} else {
|
||||
appView.showOpenFile();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -123,7 +123,11 @@ var DropboxLink = {
|
|||
}).bind(this));
|
||||
},
|
||||
|
||||
_handleUiError: function(err, callback) {
|
||||
_handleUiError: function(err, alertCallback, callback) {
|
||||
if (!alertCallback) {
|
||||
alertCallback = Alerts.error.bind(Alerts);
|
||||
}
|
||||
console.error('Dropbox error', err);
|
||||
switch (err.status) {
|
||||
case Dropbox.ApiError.INVALID_TOKEN:
|
||||
Alerts.yesno({
|
||||
|
@ -138,25 +142,25 @@ var DropboxLink = {
|
|||
});
|
||||
return;
|
||||
case Dropbox.ApiError.NOT_FOUND:
|
||||
Alerts.error({
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'The file was not found. Has it been removed from another computer?'
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.OVER_QUOTA:
|
||||
Alerts.error({
|
||||
alertCallback({
|
||||
header: 'Dropbox Full',
|
||||
body: 'Your Dropbox is full, there\'s no space left anymore.'
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.RATE_LIMITED:
|
||||
Alerts.error({
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'Too many requests to Dropbox have been made by this app. Please, try again later.'
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.NETWORK_ERROR:
|
||||
Alerts.error({
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Network Error',
|
||||
body: 'Network error occured during Dropbox sync. Please, check your connection and try again.'
|
||||
});
|
||||
|
@ -164,23 +168,22 @@ var DropboxLink = {
|
|||
case Dropbox.ApiError.INVALID_PARAM:
|
||||
case Dropbox.ApiError.OAUTH_ERROR:
|
||||
case Dropbox.ApiError.INVALID_METHOD:
|
||||
Alerts.error({
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ' + err.status
|
||||
});
|
||||
break;
|
||||
default:
|
||||
Alerts.error({
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'Something went wrong during Dropbox sync. Please, try again later. Error: ' + err
|
||||
});
|
||||
console.error('Dropbox error', err);
|
||||
break;
|
||||
}
|
||||
callback(false);
|
||||
},
|
||||
|
||||
_callAndHandleError: function(callName, args, callback) {
|
||||
_callAndHandleError: function(callName, args, callback, errorAlertCallback) {
|
||||
var that = this;
|
||||
this._getClient(function(err, client) {
|
||||
if (err) {
|
||||
|
@ -188,9 +191,9 @@ var DropboxLink = {
|
|||
}
|
||||
client[callName].apply(client, args.concat(function(err, res) {
|
||||
if (err) {
|
||||
that._handleUiError(err, function(repeat) {
|
||||
that._handleUiError(err, errorAlertCallback, function(repeat) {
|
||||
if (repeat) {
|
||||
that._callAndHandleError(callName, args, callback);
|
||||
that._callAndHandleError(callName, args, callback, errorAlertCallback);
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
|
@ -223,8 +226,8 @@ var DropboxLink = {
|
|||
}
|
||||
},
|
||||
|
||||
openFile: function(fileName, complete) {
|
||||
this._callAndHandleError('readFile', [fileName, { blob: true }], complete);
|
||||
openFile: function(fileName, complete, errorAlertCallback) {
|
||||
this._callAndHandleError('readFile', [fileName, { blob: true }], complete, errorAlertCallback);
|
||||
},
|
||||
|
||||
getFileList: function(complete) {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
var Storage = require('./storage');
|
||||
|
||||
var MaxItems = 5;
|
||||
|
||||
var LastOpenFiles = {
|
||||
all: function() {
|
||||
try {
|
||||
return JSON.parse(localStorage.lastOpenFiles).map(function(f) {
|
||||
f.dt = Date.parse(f.dt);
|
||||
return f;
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
byName: function(name) {
|
||||
return this.all().filter(function(f) { return f.name === name; })[0];
|
||||
},
|
||||
|
||||
save: function(files) {
|
||||
try {
|
||||
localStorage.lastOpenFiles = JSON.stringify(files);
|
||||
} catch (e) {
|
||||
console.error('Error saving last open files', e);
|
||||
}
|
||||
},
|
||||
|
||||
add: function(name, storage, path, availOffline) {
|
||||
console.log('Add last open file', name, storage, path);
|
||||
var files = this.all();
|
||||
files = files.filter(function(f) { return f.name !== name; });
|
||||
files.unshift({ name: name, storage: storage, path: path, availOffline: availOffline, dt: new Date() });
|
||||
while (files.length > MaxItems) {
|
||||
files.pop();
|
||||
}
|
||||
this.save(files);
|
||||
},
|
||||
|
||||
remove: function(name) {
|
||||
console.log('Remove last open file', name);
|
||||
var files = this.all();
|
||||
files.forEach(function(file) {
|
||||
if (file.name === name && file.availOffline) {
|
||||
Storage.cache.remove(file.name);
|
||||
}
|
||||
}, this);
|
||||
files = files.filter(function(file) { return file.name !== name; });
|
||||
this.save(files);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = LastOpenFiles;
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
var Storage = {};
|
||||
|
||||
[
|
||||
require('./storage-cache'),
|
||||
require('./storage-dropbox'),
|
||||
require('./storage-file')
|
||||
].forEach(function(storage) {
|
||||
Storage[storage.name] = storage;
|
||||
});
|
||||
|
||||
module.exports = Storage;
|
|
@ -0,0 +1,114 @@
|
|||
'use strict';
|
||||
|
||||
var idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
|
||||
|
||||
var StorageCache = {
|
||||
name: 'cache',
|
||||
enabled: !!idb,
|
||||
|
||||
db: null,
|
||||
errorOpening: null,
|
||||
|
||||
init: function(callback) {
|
||||
if (this.db) {
|
||||
return callback();
|
||||
}
|
||||
var that = this;
|
||||
try {
|
||||
var req = idb.open('FilesCache');
|
||||
req.onerror = function (e) {
|
||||
console.error('Error opening indexed db', e);
|
||||
that.errorOpening = e;
|
||||
callback(e);
|
||||
};
|
||||
req.onsuccess = function (e) {
|
||||
that.db = e.target.result;
|
||||
callback();
|
||||
};
|
||||
req.onupgradeneeded = function (e) {
|
||||
var db = e.target.result;
|
||||
db.createObjectStore('files');
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error opening indexed db', e);
|
||||
callback(e);
|
||||
}
|
||||
},
|
||||
|
||||
save: function(id, data, callback) {
|
||||
this.init((function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
try {
|
||||
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
|
||||
req.onsuccess = function () {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
req.onerror = function () {
|
||||
console.error('Error saving to cache', id, req.error);
|
||||
if (callback) {
|
||||
callback(req.error);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error saving to cache', id, e);
|
||||
callback(e);
|
||||
}
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
load: function(id, callback) {
|
||||
this.init((function(err) {
|
||||
if (err) {
|
||||
return callback(null, err);
|
||||
}
|
||||
try {
|
||||
var req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
|
||||
req.onsuccess = function () {
|
||||
if (callback) {
|
||||
callback(req.result);
|
||||
}
|
||||
};
|
||||
req.onerror = function () {
|
||||
console.error('Error loading from cache', id, req.error);
|
||||
if (callback) {
|
||||
callback(null, req.error);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error loading from cache', id, e);
|
||||
callback(null, e);
|
||||
}
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
remove: function(id, callback) {
|
||||
this.init((function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
try {
|
||||
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
|
||||
req.onsuccess = function () {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
req.onerror = function () {
|
||||
console.error('Error removing from cache', id, req.error);
|
||||
if (callback) {
|
||||
callback(req.error);
|
||||
}
|
||||
};
|
||||
} catch(e) {
|
||||
console.error('Error removing from cache', id, e);
|
||||
callback(e);
|
||||
}
|
||||
}).bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = StorageCache;
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../launcher');
|
||||
|
||||
var StorageDropbox = {
|
||||
name: 'dropbox',
|
||||
enabled: !Launcher
|
||||
// TODO: move Dropbox storage operations here
|
||||
};
|
||||
|
||||
module.exports = StorageDropbox;
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../launcher');
|
||||
|
||||
var StorageFile = {
|
||||
name: 'file',
|
||||
enabled: !!Launcher
|
||||
// TODO: move file storage operations here
|
||||
};
|
||||
|
||||
module.exports = StorageFile;
|
|
@ -36,9 +36,6 @@ var AppModel = Backbone.Model.extend({
|
|||
page: 'file',
|
||||
file: file
|
||||
});
|
||||
if (file.get('path')) {
|
||||
AppSettingsModel.instance.set('lastOpenFile', file.get('path'));
|
||||
}
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ var FileName = 'app-settings.json';
|
|||
var AppSettingsModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
theme: 'd',
|
||||
lastOpenFile: '',
|
||||
expandGroups: true
|
||||
},
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ var Backbone = require('backbone'),
|
|||
GroupModel = require('./group-model'),
|
||||
Launcher = require('../comp/launcher'),
|
||||
DropboxLink = require('../comp/dropbox-link'),
|
||||
Storage = require('../comp/storage'),
|
||||
LastOpenFiles = require('../comp/last-open-files'),
|
||||
kdbxweb = require('kdbxweb'),
|
||||
demoFileData = require('base64!../../resources/Demo.kdbx');
|
||||
|
||||
|
@ -26,7 +28,9 @@ var FileModel = Backbone.Model.extend({
|
|||
oldKeyFileName: '',
|
||||
passwordChanged: false,
|
||||
keyFileChanged: false,
|
||||
syncing: false
|
||||
syncing: false,
|
||||
availOffline: false,
|
||||
offline: false
|
||||
},
|
||||
|
||||
db: null,
|
||||
|
@ -65,6 +69,7 @@ var FileModel = Backbone.Model.extend({
|
|||
}
|
||||
console.log('Opened file ' + this.get('name') + ': ' + Math.round(performance.now() - start) + 'ms, ' +
|
||||
db.header.keyEncryptionRounds + ' rounds, ' + Math.round(fileData.byteLength / 1024) + ' kB');
|
||||
this.postOpen(fileData);
|
||||
}
|
||||
}).bind(this));
|
||||
} catch (e) {
|
||||
|
@ -73,12 +78,38 @@ var FileModel = Backbone.Model.extend({
|
|||
}
|
||||
},
|
||||
|
||||
postOpen: function(fileData) {
|
||||
var that = this;
|
||||
if (!this.get('offline')) {
|
||||
if (this.get('availOffline')) {
|
||||
Storage.cache.save(this.get('name'), fileData, function (err) {
|
||||
if (err) {
|
||||
that.set('availOffline', false);
|
||||
if (!that.get('storage')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
that.addToLastOpenFiles(!err);
|
||||
});
|
||||
} else {
|
||||
if (this.get('storage')) {
|
||||
this.addToLastOpenFiles(false);
|
||||
}
|
||||
Storage.cache.remove(this.get('name'));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addToLastOpenFiles: function(hasOfflineCache) {
|
||||
LastOpenFiles.add(this.get('name'), this.get('storage'), this.get('path'), hasOfflineCache);
|
||||
},
|
||||
|
||||
create: function(name) {
|
||||
var password = kdbxweb.ProtectedValue.fromString('');
|
||||
var credentials = new kdbxweb.Credentials(password);
|
||||
this.db = kdbxweb.Kdbx.create(credentials, name);
|
||||
this.readModel();
|
||||
this.set({ open: true, created: true, opening: false, error: false, name: name });
|
||||
this.set({ open: true, created: true, opening: false, error: false, name: name, offline: false });
|
||||
},
|
||||
|
||||
createDemo: function() {
|
||||
|
@ -201,7 +232,9 @@ var FileModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
getData: function(cb) {
|
||||
return this.db.save(cb);
|
||||
var data = this.db.save(cb);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
getXml: function(cb) {
|
||||
|
@ -214,6 +247,7 @@ var FileModel = Backbone.Model.extend({
|
|||
this.forEachEntry({}, function(entry) {
|
||||
entry.unsaved = false;
|
||||
});
|
||||
this.addToLastOpenFiles();
|
||||
},
|
||||
|
||||
setPassword: function(password) {
|
||||
|
|
|
@ -6,6 +6,8 @@ var Backbone = require('backbone'),
|
|||
SecureInput = require('../comp/secure-input'),
|
||||
FileModel = require('../models/file-model'),
|
||||
Launcher = require('../comp/launcher'),
|
||||
LastOpenFiles = require('../comp/last-open-files'),
|
||||
Storage = require('../comp/storage'),
|
||||
DropboxLink = require('../comp/dropbox-link');
|
||||
|
||||
var OpenView = Backbone.View.extend({
|
||||
|
@ -23,8 +25,7 @@ var OpenView = Backbone.View.extend({
|
|||
'keypress .open__pass-input': 'inputKeypress',
|
||||
'click .open__pass-enter-btn': 'openDb',
|
||||
'click .open__settings-key-file': 'openKeyFile',
|
||||
'change .open__settings-check-offline': 'changeOffline',
|
||||
'click .open__last-itemo': 'openLast',
|
||||
'click .open__last-item': 'openLast',
|
||||
'dragover': 'dragover',
|
||||
'dragleave': 'dragleave',
|
||||
'drop': 'drop'
|
||||
|
@ -49,12 +50,26 @@ var OpenView = Backbone.View.extend({
|
|||
if (this.dragTimeout) {
|
||||
clearTimeout(this.dragTimeout);
|
||||
}
|
||||
this.renderTemplate({ supportsDropbox: !Launcher });
|
||||
this.renderTemplate({ supportsDropbox: !Launcher, lastOpenFiles: this.getLastOpenFiles() });
|
||||
this.inputEl = this.$el.find('.open__pass-input');
|
||||
this.passwordInput.setElement(this.inputEl);
|
||||
return this;
|
||||
},
|
||||
|
||||
getLastOpenFiles: function() {
|
||||
return LastOpenFiles.all().map(function(f) {
|
||||
switch (f.storage) {
|
||||
case 'dropbox':
|
||||
f.icon = 'dropbox';
|
||||
break;
|
||||
default:
|
||||
f.icon = 'file-text';
|
||||
break;
|
||||
}
|
||||
return f;
|
||||
});
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
this.passwordInput.reset();
|
||||
Backbone.View.prototype.remove.apply(this, arguments);
|
||||
|
@ -96,7 +111,7 @@ var OpenView = Backbone.View.extend({
|
|||
reader.onload = (function(e) {
|
||||
this[this.reading] = e.target.result;
|
||||
if (this.reading === 'fileData') {
|
||||
this.file.set('name', file.name.replace(/\.\w+$/i, ''));
|
||||
this.file.set({ name: file.name.replace(/\.\w+$/i, ''), offline: false });
|
||||
if (file.path) {
|
||||
this.file.set({ path: file.path, storage: file.storage || 'file' });
|
||||
}
|
||||
|
@ -122,6 +137,9 @@ var OpenView = Backbone.View.extend({
|
|||
displayOpenFile: function() {
|
||||
this.$el.addClass('open--file');
|
||||
this.$el.find('#open__settings-check-offline')[0].removeAttribute('disabled');
|
||||
var canSwitchOffline = this.file.get('storage') !== 'file' && !this.file.get('offline');
|
||||
this.$el.find('.open__settings-offline').toggleClass('hide', !canSwitchOffline);
|
||||
this.$el.find('.open__settings-offline-warning').toggleClass('hide', !this.file.get('offline'));
|
||||
this.inputEl[0].removeAttribute('readonly');
|
||||
this.inputEl[0].setAttribute('placeholder', 'Password for ' + this.file.get('name'));
|
||||
this.inputEl.focus();
|
||||
|
@ -185,7 +203,7 @@ var OpenView = Backbone.View.extend({
|
|||
|
||||
openFile: function() {
|
||||
if (!this.file.get('opening')) {
|
||||
this.openAny('fileData'/*, '.kdbx'*/);
|
||||
this.openAny('fileData');
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -222,6 +240,11 @@ var OpenView = Backbone.View.extend({
|
|||
|
||||
openDb: function() {
|
||||
if (!this.file.get('opening') && this.file.get('name')) {
|
||||
var offlineChecked = this.$el.find('#open__settings-check-offline').is(':checked');
|
||||
if (this.file.get('offline') ||
|
||||
this.file.get('storage') !== 'file' && offlineChecked) {
|
||||
this.file.set('availOffline', true);
|
||||
}
|
||||
var arg = {
|
||||
password: this.passwordInput.value,
|
||||
fileData: this.fileData,
|
||||
|
@ -234,12 +257,6 @@ var OpenView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
changeOffline: function(e) {
|
||||
if (!this.file.get('opening')) {
|
||||
this.file.set('availOffline', !!e.target.checked);
|
||||
}
|
||||
},
|
||||
|
||||
inputKeydown: function(e) {
|
||||
var code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_RETURN && this.passwordInput.length) {
|
||||
|
@ -265,6 +282,142 @@ var OpenView = Backbone.View.extend({
|
|||
this.$el.find('.open__file-warning').toggleClass('invisible', on);
|
||||
},
|
||||
|
||||
openFromDropbox: function() {
|
||||
if (this.dropboxLoading || this.file.get('opening')) {
|
||||
return;
|
||||
}
|
||||
var that = this;
|
||||
DropboxLink.authenticate(function(err) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
that.dropboxLoading = 'file list';
|
||||
that.displayDropboxLoading();
|
||||
DropboxLink.getFileList(function(err, files) {
|
||||
that.dropboxLoading = null;
|
||||
that.displayDropboxLoading();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
var buttons = [];
|
||||
var allFileNames = {};
|
||||
files.forEach(function(file) {
|
||||
var fileName = file.replace(/\.kdbx/i, '');
|
||||
buttons.push({ result: file, title: fileName });
|
||||
allFileNames[fileName] = true;
|
||||
});
|
||||
if (!buttons.length) {
|
||||
Alerts.error({
|
||||
header: 'Nothing found',
|
||||
body: 'You have no files in your Dropbox which could be opened. Files are searched in your Dropbox app folder: Apps/KeeWeb'
|
||||
});
|
||||
return;
|
||||
}
|
||||
buttons.push({ result: '', title: 'Cancel' });
|
||||
Alerts.alert({
|
||||
header: 'Select a file',
|
||||
body: 'Select a file from your Dropbox which you would like to open',
|
||||
icon: 'dropbox',
|
||||
buttons: buttons,
|
||||
esc: '',
|
||||
click: '',
|
||||
success: that.openDropboxFile.bind(that),
|
||||
cancel: function() {
|
||||
that.dropboxLoading = null;
|
||||
that.displayDropboxLoading();
|
||||
}
|
||||
});
|
||||
LastOpenFiles.all().forEach(function(lastOpenFile) {
|
||||
if (lastOpenFile.storage === 'dropbox' && !allFileNames[lastOpenFile.name]) {
|
||||
that.delLast(lastOpenFile.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
openDropboxFile: function(file) {
|
||||
var fileName = file.replace(/\.kdbx/i, '');
|
||||
this.dropboxLoading = fileName;
|
||||
this.displayDropboxLoading();
|
||||
var lastOpen = LastOpenFiles.byName(fileName);
|
||||
var errorAlertCallback = lastOpen && lastOpen.storage === 'dropbox' && lastOpen.availOffline ?
|
||||
this.dropboxErrorCallback.bind(this, fileName) : null;
|
||||
DropboxLink.openFile(file, (function(err, data) {
|
||||
this.dropboxLoading = null;
|
||||
this.displayDropboxLoading();
|
||||
if (err || !data || !data.size) {
|
||||
return;
|
||||
}
|
||||
Object.defineProperties(data, {
|
||||
storage: { value: 'dropbox' },
|
||||
path: { value: file },
|
||||
name: { value: fileName }
|
||||
});
|
||||
this.setFile(data);
|
||||
}).bind(this), errorAlertCallback);
|
||||
},
|
||||
|
||||
dropboxErrorCallback: function(fileName, alertConfig) {
|
||||
alertConfig.body += '<br/>You have offline version of this file cached. Would you like to open it?';
|
||||
alertConfig.buttons = [
|
||||
{result: 'offline', title: 'Open offline file'},
|
||||
{result: 'yes', title: 'OK'}
|
||||
];
|
||||
alertConfig.success = (function(result) {
|
||||
if (result === 'offline') {
|
||||
this.openCache(fileName, 'dropbox');
|
||||
}
|
||||
}).bind(this);
|
||||
Alerts.error(alertConfig);
|
||||
},
|
||||
|
||||
displayDropboxLoading: function() {
|
||||
this.$el.find('.open__icon-dropbox .open__icon-i').toggleClass('flip3d', !!this.dropboxLoading);
|
||||
},
|
||||
|
||||
openLast: function(e) {
|
||||
if (this.dropboxLoading || this.file.get('opening')) {
|
||||
return;
|
||||
}
|
||||
var name = $(e.target).closest('.open__last-item').data('name');
|
||||
if ($(e.target).is('.open__last-item-icon-del')) {
|
||||
this.delLast(name);
|
||||
return;
|
||||
}
|
||||
var lastOpenFile = LastOpenFiles.byName(name);
|
||||
switch (lastOpenFile.storage) {
|
||||
case 'dropbox':
|
||||
return this.openDropboxFile(lastOpenFile.path);
|
||||
default:
|
||||
return this.openCache(name);
|
||||
}
|
||||
},
|
||||
|
||||
openCache: function(name, storage) {
|
||||
Storage.cache.load(name, (function(data, err) {
|
||||
if (err) {
|
||||
this.delLast(name);
|
||||
Alerts.error({
|
||||
header: 'Error loading file',
|
||||
body: 'There was an error loading offline file ' + name + '. Please, open it from file'
|
||||
});
|
||||
} else {
|
||||
this.fileData = data;
|
||||
this.file.set({ name: name, offline: true, availOffline: true });
|
||||
if (storage) {
|
||||
this.file.set({ storage: storage });
|
||||
}
|
||||
this.displayOpenFile();
|
||||
}
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
delLast: function(name) {
|
||||
LastOpenFiles.remove(name);
|
||||
this.$el.find('.open__last-item[data-name="' + name + '"]').remove();
|
||||
},
|
||||
|
||||
dragover: function(e) {
|
||||
e.preventDefault();
|
||||
if (this.dragTimeout) {
|
||||
|
@ -296,77 +449,6 @@ var OpenView = Backbone.View.extend({
|
|||
if (dataFile) {
|
||||
this.setFile(dataFile, keyFile);
|
||||
}
|
||||
},
|
||||
|
||||
openFromDropbox: function() {
|
||||
if (this.dropboxLoading) {
|
||||
return;
|
||||
}
|
||||
var that = this;
|
||||
DropboxLink.authenticate(function(err) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
that.dropboxLoading = 'file list';
|
||||
that.displayDropboxLoading();
|
||||
DropboxLink.getFileList(function(err, files) {
|
||||
that.dropboxLoading = null;
|
||||
that.displayDropboxLoading();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
var buttons = [];
|
||||
files.forEach(function(file) {
|
||||
buttons.push({ result: file, title: file.replace(/\.kdbx/i, '') });
|
||||
});
|
||||
if (!buttons.length) {
|
||||
Alerts.error({
|
||||
header: 'Nothing found',
|
||||
body: 'You have no files in your Dropbox which could be opened. Files are searched in your Dropbox app folder: Apps/KeeWeb'
|
||||
});
|
||||
return;
|
||||
}
|
||||
buttons.push({ result: '', title: 'Cancel' });
|
||||
Alerts.alert({
|
||||
header: 'Select a file',
|
||||
body: 'Select a file from your Dropbox which you would like to open',
|
||||
icon: 'dropbox',
|
||||
buttons: buttons,
|
||||
esc: '',
|
||||
click: '',
|
||||
success: that.openDropboxFile.bind(that),
|
||||
cancel: function() {
|
||||
that.dropboxLoading = null;
|
||||
that.displayDropboxLoading();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
openDropboxFile: function(file) {
|
||||
var fileName = file.replace(/\.kdbx/i, '');
|
||||
this.dropboxLoading = fileName;
|
||||
this.displayDropboxLoading();
|
||||
DropboxLink.openFile(file, (function(err, data) {
|
||||
this.dropboxLoading = null;
|
||||
this.displayDropboxLoading();
|
||||
if (err || !data || !data.size) {
|
||||
// TODO:render
|
||||
Alerts.error({ header: 'Failed to read file', body: 'Error reading Dropbox file: \n' + err });
|
||||
return;
|
||||
}
|
||||
Object.defineProperties(data, {
|
||||
storage: { value: 'dropbox' },
|
||||
path: { value: file },
|
||||
name: { value: fileName }
|
||||
});
|
||||
this.setFile(data);
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
displayDropboxLoading: function() {
|
||||
this.$el.find('.open__icon-dropbox .open__icon-i').toggleClass('flip3d', !!this.dropboxLoading);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -127,9 +127,6 @@ var SettingsAboutView = Backbone.View.extend({
|
|||
Launcher.writeFile(path, data);
|
||||
this.passwordChanged = false;
|
||||
this.model.saved(path, 'file');
|
||||
if (!AppSettingsModel.instance.get('lastOpenFile')) {
|
||||
AppSettingsModel.instance.set('lastOpenFile', path);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error saving file', path, e);
|
||||
Alerts.error({
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
@include justify-content(space-between);
|
||||
@include align-items(stretch);
|
||||
&-key-file {
|
||||
height: 2em;
|
||||
.open--file:not(.open--opening) & { cursor: pointer; }
|
||||
.open--key-file & { @include th { color: medium-color(); } }
|
||||
&-dropbox {
|
||||
|
@ -127,12 +128,30 @@
|
|||
@include align-items(stretch);
|
||||
margin-top: $base-spacing;
|
||||
&-item {
|
||||
@include display(flex);
|
||||
@include flex-direction(row);
|
||||
@include justify-content(flex-start);
|
||||
@include align-items(baseline);
|
||||
.open:not(.open--opening) & {
|
||||
@include area-selectable;
|
||||
}
|
||||
@include th { color: muted-color(); }
|
||||
padding: $base-padding-v 0;
|
||||
>i { width: 2em; }
|
||||
&-icon { width: 2em; }
|
||||
&-text { @include flex-grow(1); }
|
||||
&-icon-del {
|
||||
opacity: 0;
|
||||
.open__last-item:hover & {
|
||||
opacity: .3;
|
||||
cursor: pointer;
|
||||
margin-right: $base-padding-h;
|
||||
}
|
||||
@include th { color: muted-color(); }
|
||||
&:hover {
|
||||
.open__last-item:hover & { opacity: 1; }
|
||||
@include th { color: medium-color(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ button.pika-next {
|
|||
}
|
||||
|
||||
.pika-table th {
|
||||
@include th { color: medium-color(); }; // TODO
|
||||
@include th { color: medium-color(); };
|
||||
}
|
||||
|
||||
.pika-button, button.pika-button {
|
||||
|
|
|
@ -38,18 +38,20 @@
|
|||
<span class="open__settings-key-file-dropbox"> (from dropbox)</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="open__settings-offline">
|
||||
<div class="open__settings-offline hide">
|
||||
<input type="checkbox" id="open__settings-check-offline" class="open__settings-check-offline" checked disabled />
|
||||
<% if (supportsDropbox) { %>
|
||||
<label for="open__settings-check-offline" class="open__settings-label-offline">make available offline</label>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="open__settings-offline-warning hide muted-color"><i class="fa fa-exclamation-triangle"></i> saved offline version</div>
|
||||
</div>
|
||||
<div class="open__last">
|
||||
<!--<div class="open__last-item"><i class="fa fa-dropbox"></i>My passwords</div>-->
|
||||
<!--<div class="open__last-item"><i class="fa fa-dropbox"></i>Work</div>-->
|
||||
<!--<div class="open__last-item"><i class="fa fa-dropbox"></i>Hard with many rounds</div>-->
|
||||
<!--<div class="open__last-item"><i class="fa fa-file-text"></i>Local</div>-->
|
||||
<% lastOpenFiles.forEach(function(file) { %>
|
||||
<div class="open__last-item" data-name="<%- file.name %>">
|
||||
<i class="fa fa-<%= file.icon %> open__last-item-icon"></i>
|
||||
<span class="open__last-item-text"><%- file.name %></span>
|
||||
<i class="fa fa-times open__last-item-icon-del"></i>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="open__dropzone">
|
||||
|
|
Loading…
Reference in New Issue