Merge branch 'develop'

This commit is contained in:
Antelle 2015-12-14 01:02:28 +03:00
commit 7c89717991
68 changed files with 1695 additions and 930 deletions

6
.idea/encodings.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View File

@ -88,8 +88,6 @@
"globals" : {
"require": true,
"module": true,
"console": true,
"performance": true,
"$": true,
"_": true
}

View File

@ -16,7 +16,7 @@ module.exports = function(grunt) {
var webpack = require('webpack');
var pkg = require('./package.json');
var dt = new Date().toISOString().replace(/T.*/, '');
var electronVersion = '0.35.1';
var electronVersion = '0.36.0';
function replaceFont(css) {
css.walkAtRules('font-face', function (rule) {
@ -152,12 +152,15 @@ module.exports = function(grunt) {
manifest: {
options: {
replacements: [
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version },
{ pattern: 'vElectron', replacement: electronVersion }
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version }
]
},
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
},
'manifest_html': {
options: { replacements: [{ pattern: '<html', replacement: '<html manifest="manifest.appcache"' }] },
files: { 'dist/index.html': 'dist/index.html' }
},
'desktop_html': {
options: { replacements: [{ pattern: ' manifest="manifest.appcache"', replacement: '' }] },
files: { 'tmp/desktop/app/index.html': 'dist/index.html' }
@ -373,6 +376,7 @@ module.exports = function(grunt) {
'postcss',
'inline',
'htmlmin',
'string-replace:manifest_html',
'string-replace:manifest'
]);

View File

@ -1,7 +1,7 @@
# KeePass web app (unofficial)
This webapp is a browser and desktop password manager compatible with KeePass databases. It doesn't require any server or additional resources.
The app can run either in browser, or as a desktop app.
The app can run either in browser, or as a desktop app.
![screenshot](https://habrastorage.org/files/bfb/51e/d8d/bfb51ed8d19847d8afb827c4fbff7dd5.png)
@ -14,14 +14,8 @@ Twitter: [kee_web](https://twitter.com/kee_web)
# Status
Reading and display is mostly complete; modification and sync is under construction, please see [TODO](https://github.com/antelle/keeweb/wiki/TODO) for more details.
# Known Issues
These major issues are in progress, or will be fixed in next releases, before v1.0:
- dropbox sync is one-way: changes are not loaded from dropbox, only saved
- files are considered saved only when they are exported
The app is already rather stable but might still need polishing, testing and improvements before v1 release, which is expected to happen in Feb 2016.
Please see [TODO](https://github.com/antelle/keeweb/wiki/TODO) for more details.
# Self-hosting
@ -31,17 +25,17 @@ To make Dropbox work in your self-hosted app:
1. [create](https://www.dropbox.com/developers/apps/create) a Dropbox app
2. find your app key (in Dropbox App page, go to Settings/App key)
3. change Dropbox app key in index.html file: `sed -i.bak s/qp7ctun6qt5n9d6/your_app_key/g index.html`
3. change Dropbox app key in index.html file: `sed -i.bak s/qp7ctun6qt5n9d6/your_app_key/g index.html`
(or, if you are building from source, change it [here](scripts/comp/dropbox-link.js#L7))
# Building
The app can be built with grunt: `grunt` (html file will be in `dist/`).
Desktop apps are built with `grunt desktop`. This works only in mac osx as it builds dmg; requires wine.
To run Electron app without building, install electron package (`npm install electron-prebuilt -g`) and start in this way:
To run Electron app without building installer, install electron package (`npm install electron-prebuilt -g`), build the app with `grunt` and start in this way:
```bash
$ cd electron
$ electron . --htmlpath=../tmp
$ grunt
$ electron electron --htmlpath=tmp
```
For debug build:

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html manifest="manifest.appcache">
<html>
<head lang="en">
<meta charset="UTF-8">
<title>KeeWeb</title>

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# YYYY-MM-DD:v0.0.0 evElectron
# YYYY-MM-DD:v0.0.0
CACHE:
index.html

View File

@ -7,7 +7,6 @@ var AppModel = require('./models/app-model'),
Alerts = require('./comp/alerts'),
DropboxLink = require('./comp/dropbox-link'),
Updater = require('./comp/updater'),
LastOpenFiles = require('./comp/last-open-files'),
ThemeChanger = require('./util/theme-changer');
$(function() {
@ -41,15 +40,7 @@ $(function() {
}
function showApp() {
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();
}
new AppView({ model: appModel }).render();
Updater.init();
}
});

View File

@ -25,27 +25,10 @@ var EntryCollection = Backbone.Collection.extend({
defaultComparator: 'title',
activeEntry: null,
initialize: function() {
this.comparator = this.comparators[this.defaultComparator];
},
setActive: function(entry) {
if (!(entry instanceof EntryModel)) {
entry = this.get(entry);
}
this.forEach(function(entry) { entry.active = false; });
if (entry) {
entry.active = true;
}
this.activeEntry = entry;
},
getActive: function() {
return this.activeEntry;
},
sortEntries: function(comparator) {
this.comparator = this.comparators[comparator] || this.comparators[this.defaultComparator];
this.sort();

View File

@ -14,8 +14,16 @@ var FileCollection = Backbone.Collection.extend({
return this.some(function(file) { return file.get('modified'); });
},
hasDirtyFiles: function() {
return this.some(function(file) { return file.get('dirty'); });
},
getByName: function(name) {
return this.find(function(file) { return file.get('name') === name; });
return this.find(function(file) { return file.get('name').toLowerCase() === name.toLowerCase(); });
},
getById: function(id) {
return this.find(function(file) { return file.get('id') === id; });
}
});

View File

@ -0,0 +1,47 @@
'use strict';
var Backbone = require('backbone'),
FileInfoModel = require('../models/file-info-model'),
SettingsStore = require('../comp/settings-store');
var FileInfoCollection = Backbone.Collection.extend({
model: FileInfoModel,
initialize: function () {
},
load: function () {
var data = SettingsStore.load('file-info');
if (data) {
this.reset(data, {silent: true});
}
},
save: function () {
SettingsStore.save('file-info', this.toJSON());
},
getLast: function () {
return this.first();
},
getMatch: function (storage, name, path) {
return this.find(function(fi) {
return (fi.get('storage') || '') === (storage || '') &&
(fi.get('name') || '') === (name || '') &&
(fi.get('path') || '') === (path || '');
});
},
getByName: function(name) {
return this.find(function(file) { return file.get('name').toLowerCase() === name.toLowerCase(); });
}
});
FileInfoCollection.load = function() {
var coll = new FileInfoCollection();
coll.load();
return coll;
};
module.exports = FileInfoCollection;

View File

@ -3,8 +3,11 @@
var Dropbox = require('dropbox'),
Alerts = require('./alerts'),
Launcher = require('./launcher'),
Logger = require('../util/logger'),
Links = require('../const/links');
var logger = new Logger('dropbox');
var DropboxKeys = {
AppFolder: 'qp7ctun6qt5n9d6',
AppFolderKeyParts: ['qp7ctun6', 'qt5n9d6'] // to allow replace key by sed, compare in this way
@ -121,6 +124,8 @@ DropboxChooser.prototype.readFile = function(url) {
};
var DropboxLink = {
ERROR_CONFLICT: Dropbox.ApiError.CONFLICT,
ERROR_NOT_FOUND: Dropbox.ApiError.NOT_FOUND,
_getClient: function(complete) {
if (this._dropboxClient && this._dropboxClient.isAuthenticated()) {
complete(null, this._dropboxClient);
@ -151,22 +156,29 @@ var DropboxLink = {
_handleUiError: function(err, alertCallback, callback) {
if (!alertCallback) {
alertCallback = Alerts.error.bind(Alerts);
if (!Alerts.alertDisplayed) {
alertCallback = Alerts.error.bind(Alerts);
}
}
console.error('Dropbox error', err);
logger.error('Dropbox error', err);
switch (err.status) {
case Dropbox.ApiError.INVALID_TOKEN:
Alerts.yesno({
icon: 'dropbox',
header: 'Dropbox Login',
body: 'To continue, you have to sign in to Dropbox.',
buttons: [{result: 'yes', title: 'Sign In'}, {result: '', title: 'Cancel'}],
success: (function() {
this.authenticate(function(err) { callback(!err); });
}).bind(this),
cancel: function() { callback(false); }
});
return;
if (!Alerts.alertDisplayed) {
Alerts.yesno({
icon: 'dropbox',
header: 'Dropbox Login',
body: 'To continue, you have to sign in to Dropbox.',
buttons: [{result: 'yes', title: 'Sign In'}, {result: '', title: 'Cancel'}],
success: (function () {
this.authenticate(function (err) { callback(!err); });
}).bind(this),
cancel: function () {
callback(false);
}
});
return;
}
break;
case Dropbox.ApiError.NOT_FOUND:
alertCallback({
header: 'Dropbox Sync Error',
@ -199,6 +211,8 @@ var DropboxLink = {
body: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ' + err.status
});
break;
case Dropbox.ApiError.CONFLICT:
break;
default:
alertCallback({
header: 'Dropbox Sync Error',
@ -215,7 +229,10 @@ var DropboxLink = {
if (err) {
return callback(err);
}
var ts = logger.ts();
logger.debug('Call', callName);
client[callName].apply(client, args.concat(function(err) {
logger.debug('Result', callName, logger.ts(ts), arguments);
if (err) {
that._handleUiError(err, errorAlertCallback, function(repeat) {
if (repeat) {
@ -239,9 +256,10 @@ var DropboxLink = {
Dropbox.AuthDriver.Popup.oauthReceiver();
},
saveFile: function(fileName, data, overwrite, complete) {
if (overwrite) {
this._callAndHandleError('writeFile', [fileName, data], complete);
saveFile: function(fileName, data, rev, complete, alertCallback) {
if (rev) {
var opts = typeof rev === 'string' ? { lastVersionTag: rev, noOverwrite: true, noAutoRename: true } : undefined;
this._callAndHandleError('writeFile', [fileName, data, opts], complete, alertCallback);
} else {
this.getFileList((function(err, files) {
if (err) { return complete(err); }
@ -253,18 +271,26 @@ var DropboxLink = {
},
openFile: function(fileName, complete, errorAlertCallback) {
this._callAndHandleError('readFile', [fileName, { blob: true }], complete, errorAlertCallback);
this._callAndHandleError('readFile', [fileName, { arrayBuffer: true }], complete, errorAlertCallback);
},
stat: function(fileName, complete, errorAlertCallback) {
this._callAndHandleError('stat', [fileName], complete, errorAlertCallback);
},
getFileList: function(complete) {
this._callAndHandleError('readdir', [''], function(err, files, dirStat) {
this._callAndHandleError('readdir', [''], function(err, files, dirStat, filesStat) {
if (files) {
files = files.filter(function(f) { return /\.kdbx$/i.test(f); });
}
complete(err, files, dirStat);
complete(err, files, dirStat, filesStat);
});
},
deleteFile: function(fileName, complete) {
this._callAndHandleError('remove', [fileName], complete);
},
chooseFile: function(callback) {
new DropboxChooser(callback).choose();
}

View File

@ -1,55 +0,0 @@
'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;

View File

@ -49,6 +49,9 @@ if (window.process && window.process.versions && window.process.versions.electro
fileExists: function(path) {
return this.req('fs').existsSync(path);
},
deleteFile: function(path) {
this.req('fs').unlinkSync(path);
},
preventExit: function(e) {
e.returnValue = false;
return false;
@ -88,6 +91,9 @@ if (window.process && window.process.versions && window.process.versions.electro
Backbone.on('launcher-exit-request', function() {
setTimeout(function() { Launcher.exit(); }, 0);
});
Backbone.on('launcher-minimize', function() {
setTimeout(function() { Backbone.trigger('app-minimized'); }, 0);
});
window.launcherOpen = function(path) {
Backbone.trigger('launcher-open-file', path);
};

View File

@ -0,0 +1,44 @@
'use strict';
var Launcher = require('./launcher'),
StringUtil = require('../util/string-util'),
Logger = require('../util/logger');
var logger = new Logger('settings');
var SettingsStore = {
fileName: function(key) {
return key + '.json';
},
load: function(key) {
try {
if (Launcher) {
var settingsFile = Launcher.getUserDataPath(this.fileName(key));
if (Launcher.fileExists(settingsFile)) {
return JSON.parse(Launcher.readFile(settingsFile, 'utf8'));
}
} else {
var data = localStorage[StringUtil.camelCase(key)];
return data ? JSON.parse(data) : undefined;
}
} catch (e) {
logger.error('Error loading ' + key, e);
}
return null;
},
save: function(key, data) {
try {
if (Launcher) {
Launcher.writeFile(Launcher.getUserDataPath(this.fileName(key)), JSON.stringify(data));
} else if (typeof localStorage !== 'undefined') {
localStorage[StringUtil.camelCase(key)] = JSON.stringify(data);
}
} catch (e) {
logger.error('Error saving ' + key, e);
}
}
};
module.exports = SettingsStore;

View File

@ -1,13 +0,0 @@
'use strict';
var Storage = {};
[
require('./storage-cache'),
require('./storage-dropbox'),
require('./storage-file')
].forEach(function(storage) {
Storage[storage.name] = storage;
});
module.exports = Storage;

View File

@ -1,11 +0,0 @@
'use strict';
var Launcher = require('../launcher');
var StorageDropbox = {
name: 'dropbox',
enabled: !Launcher
// TODO: move Dropbox storage operations here
};
module.exports = StorageDropbox;

View File

@ -1,11 +0,0 @@
'use strict';
var Launcher = require('../launcher');
var StorageFile = {
name: 'file',
enabled: !!Launcher
// TODO: move file storage operations here
};
module.exports = StorageFile;

View File

@ -1,6 +1,9 @@
'use strict';
var Launcher = require('./launcher');
var Launcher = require('./launcher'),
Logger = require('../util/logger');
var logger = new Logger('transport');
var Transport = {
httpGet: function(config) {
@ -11,7 +14,7 @@ var Transport = {
if (fs.existsSync(tmpFile)) {
try {
if (config.cache && fs.statSync(tmpFile).size > 0) {
console.log('File already downloaded ' + config.url);
logger.info('File already downloaded ' + config.url);
return config.success(tmpFile);
} else {
fs.unlinkSync(tmpFile);
@ -22,11 +25,11 @@ var Transport = {
}
}
var proto = config.url.split(':')[0];
console.log('GET ' + config.url);
logger.info('GET ' + config.url);
var opts = Launcher.req('url').parse(config.url);
opts.headers = { 'User-Agent': navigator.userAgent };
Launcher.req(proto).get(opts, function(res) {
console.log('Response from ' + config.url + ': ' + res.statusCode);
logger.info('Response from ' + config.url + ': ' + res.statusCode);
if (res.statusCode === 200) {
if (config.file) {
var file = fs.createWriteStream(tmpFile);
@ -57,7 +60,7 @@ var Transport = {
config.error('HTTP status ' + res.statusCode);
}
}).on('error', function(e) {
console.error('Cannot GET ' + config.url, e);
logger.error('Cannot GET ' + config.url, e);
if (tmpFile) {
fs.unlink(tmpFile);
}

View File

@ -6,7 +6,10 @@ var Backbone = require('backbone'),
Launcher = require('../comp/launcher'),
AppSettingsModel = require('../models/app-settings-model'),
UpdateModel = require('../models/update-model'),
Transport = require('../comp/transport');
Transport = require('../comp/transport'),
Logger = require('../util/logger');
var logger = new Logger('updater');
var Updater = {
UpdateInterval: 1000*60*60*24,
@ -57,7 +60,7 @@ var Updater = {
timeDiff = Math.min(Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout), this.UpdateInterval);
}
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
console.log('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
return timeDiff === this.MinUpdateTimeout;
},
@ -74,20 +77,20 @@ var Updater = {
// additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
var diffMs = new Date() - this.updateCheckDate;
if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
console.error('Prevented update check; last check was performed at ' + this.updateCheckDate);
logger.error('Prevented update check; last check was performed at ' + this.updateCheckDate);
that.scheduleNextCheck();
return;
}
this.updateCheckDate = new Date();
}
console.log('Checking for update...');
logger.info('Checking for update...');
Transport.httpGet({
url: Links.WebApp + 'manifest.appcache',
utf8: true,
success: function(data) {
var dt = new Date();
var match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
console.log('Update check: ' + (match ? match[0] : 'unknown'));
logger.info('Update check: ' + (match ? match[0] : 'unknown'));
if (!match) {
var errMsg = 'No version info found';
UpdateModel.instance.set({ status: 'error', lastCheckDate: dt, lastCheckError: errMsg });
@ -108,7 +111,7 @@ var Updater = {
that.scheduleNextCheck();
if (prevLastVersion === UpdateModel.instance.get('lastVersion') &&
UpdateModel.instance.get('updateStatus') === 'ready') {
console.log('Waiting for the user to apply downloaded update');
logger.info('Waiting for the user to apply downloaded update');
return;
}
if (!startedByUser && that.getAutoUpdateType() === 'install') {
@ -118,7 +121,7 @@ var Updater = {
}
},
error: function(e) {
console.error('Update check error', e);
logger.error('Update check error', e);
UpdateModel.instance.set({
status: 'error',
lastCheckDate: new Date(),
@ -140,22 +143,22 @@ var Updater = {
update: function(startedByUser, successCallback) {
var ver = UpdateModel.instance.get('lastVersion');
if (!Launcher || ver === RuntimeInfo.version) {
console.log('You are using the latest version');
logger.info('You are using the latest version');
return;
}
UpdateModel.instance.set({ updateStatus: 'downloading', updateError: null });
var that = this;
console.log('Downloading update', ver);
logger.info('Downloading update', ver);
Transport.httpGet({
url: Links.UpdateDesktop.replace('{ver}', ver),
file: 'KeeWeb-' + ver + '.zip',
cache: !startedByUser,
success: function(filePath) {
UpdateModel.instance.set('updateStatus', 'extracting');
console.log('Extracting update file', that.UpdateCheckFiles, filePath);
logger.info('Extracting update file', that.UpdateCheckFiles, filePath);
that.extractAppUpdate(filePath, function(err) {
if (err) {
console.error('Error extracting update', err);
logger.error('Error extracting update', err);
UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error extracting update' });
} else {
UpdateModel.instance.set({ updateStatus: 'ready', updateError: null });
@ -169,7 +172,7 @@ var Updater = {
});
},
error: function(e) {
console.error('Error downloading update', e);
logger.error('Error downloading update', e);
UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error downloading update' });
}
});

View File

@ -0,0 +1,7 @@
'use strict';
var Timeouts = {
AutoSync: 30 * 1000 * 60
};
module.exports = Timeouts;

View File

@ -6,7 +6,13 @@ var Backbone = require('backbone'),
EntryModel = require('./entry-model'),
GroupModel = require('./group-model'),
FileCollection = require('../collections/file-collection'),
EntryCollection = require('../collections/entry-collection');
EntryCollection = require('../collections/entry-collection'),
FileInfoCollection = require('../collections/file-info-collection'),
FileModel = require('./file-model'),
FileInfoModel = require('./file-info-model'),
Storage = require('../storage'),
IdGenerator = require('../util/id-generator'),
Logger = require('../util/logger');
var AppModel = Backbone.Model.extend({
defaults: {},
@ -14,23 +20,31 @@ var AppModel = Backbone.Model.extend({
initialize: function() {
this.tags = [];
this.files = new FileCollection();
this.fileInfos = FileInfoCollection.load();
this.menu = new MenuModel();
this.filter = {};
this.sort = 'title';
this.settings = AppSettingsModel.instance;
this.activeEntryId = null;
this.listenTo(Backbone, 'refresh', this.refresh);
this.listenTo(Backbone, 'set-filter', this.setFilter);
this.listenTo(Backbone, 'add-filter', this.addFilter);
this.listenTo(Backbone, 'set-sort', this.setSort);
this.listenTo(Backbone, 'close-file', this.closeFile);
this.listenTo(Backbone, 'empty-trash', this.emptyTrash);
this.appLogger = new Logger('app');
},
addFile: function(file) {
if (this.files.getById(file.id)) {
return false;
}
this.files.add(file);
file.get('groups').forEach(function(group) { this.menu.groupsSection.addItem(group); }, this);
this._addTags(file.db);
file.get('groups').forEach(function (group) {
this.menu.groupsSection.addItem(group);
}, this);
this._addTags(file);
this._tagsChanged();
this.menu.filesSection.addItem({
icon: 'lock',
@ -39,24 +53,32 @@ var AppModel = Backbone.Model.extend({
file: file
});
this.refresh();
this.listenTo(file, 'reload', this.reloadFile);
return true;
},
_addTags: function(group) {
reloadFile: function(file) {
this.menu.groupsSection.removeByFile(file, true);
file.get('groups').forEach(function (group) {
this.menu.groupsSection.addItem(group);
}, this);
this.updateTags();
},
_addTags: function(file) {
var tagsHash = {};
this.tags.forEach(function(tag) {
tagsHash[tag.toLowerCase()] = true;
});
_.forEach(group.entries, function(entry) {
var that = this;
file.forEachEntry({}, function(entry) {
_.forEach(entry.tags, function(tag) {
if (!tagsHash[tag.toLowerCase()]) {
tagsHash[tag.toLowerCase()] = true;
this.tags.push(tag);
that.tags.push(tag);
}
}, this);
}, this);
_.forEach(group.groups, function(subGroup) {
this._addTags(subGroup);
}, this);
});
});
this.tags.sort();
},
@ -76,7 +98,7 @@ var AppModel = Backbone.Model.extend({
var oldTags = this.tags.slice();
this.tags.splice(0, this.tags.length);
this.files.forEach(function(file) {
this._addTags(file.db);
this._addTags(file);
}, this);
if (!_.isEqual(oldTags, this.tags)) {
this._tagsChanged();
@ -91,12 +113,12 @@ var AppModel = Backbone.Model.extend({
this.menu.tagsSection.removeAllItems();
this.menu.filesSection.removeAllItems();
this.tags.splice(0, this.tags.length);
this.setFilter({});
this.filter = {};
},
closeFile: function(file) {
this.files.remove(file);
this._tagsChanged();
this.updateTags();
this.menu.groupsSection.removeByFile(file);
this.menu.filesSection.removeByFile(file);
this.refresh();
@ -113,8 +135,12 @@ var AppModel = Backbone.Model.extend({
this.filter = filter;
this.filter.subGroups = this.settings.get('expandGroups');
var entries = this.getEntries();
if (!this.activeEntryId || !entries.get(this.activeEntryId)) {
var firstEntry = entries.first();
this.activeEntryId = firstEntry ? firstEntry.id : null;
}
Backbone.trigger('filter', { filter: this.filter, sort: this.sort, entries: entries });
Backbone.trigger('select-entry', entries.length ? entries.first() : null);
Backbone.trigger('select-entry', entries.get(this.activeEntryId));
},
refresh: function() {
@ -142,9 +168,6 @@ var AppModel = Backbone.Model.extend({
if (this.filter.trash) {
this.addTrashGroups(entries);
}
if (entries.length) {
entries.setActive(entries.first());
}
return entries;
},
@ -197,6 +220,374 @@ var AppModel = Backbone.Model.extend({
createNewGroup: function() {
var sel = this.getFirstSelectedGroup();
return GroupModel.newGroup(sel.group, sel.file);
},
createDemoFile: function() {
var that = this;
if (!this.files.getByName('Demo')) {
var demoFile = new FileModel();
demoFile.openDemo(function() {
that.addFile(demoFile);
});
return true;
} else {
return false;
}
},
createNewFile: function() {
var name;
for (var i = 0; ; i++) {
name = 'New' + (i || '');
if (!this.files.getByName(name) && !this.fileInfos.getByName(name)) {
break;
}
}
var newFile = new FileModel();
newFile.create(name);
this.addFile(newFile);
},
openFile: function(params, callback) {
var logger = new Logger('open', params.name);
logger.info('File open request');
var that = this;
var fileInfo = params.id ? this.fileInfos.get(params.id) : this.fileInfos.getMatch(params.storage, params.name, params.path);
if (fileInfo && fileInfo.get('modified')) {
// modified offline, cannot overwrite: load from cache
logger.info('Open file from cache because it is modified');
this.openFileFromCache(params, callback, fileInfo);
} else if (params.fileData) {
// has user content: load it
logger.info('Open file from supplied content');
this.openFileWithData(params, callback, fileInfo, params.fileData, true);
} else if (!params.storage) {
// no storage: load from cache as main storage
logger.info('Open file from cache as main storage');
this.openFileFromCache(params, callback, fileInfo);
} else if (fileInfo && fileInfo.get('rev') === params.rev && fileInfo.get('storage') !== 'file') {
// already latest in cache: use it
logger.info('Open file from cache because it is latest');
this.openFileFromCache(params, callback, fileInfo);
} else {
// try to load from storage and update cache
logger.info('Open file from storage', params.storage);
var storage = Storage[params.storage];
var storageLoad = function() {
logger.info('Load from storage');
storage.load(params.path, function(err, data, stat) {
if (err) {
// failed to load from storage: fallback to cache if we can
if (fileInfo) {
logger.info('Open file from cache because of storage load error', err);
that.openFileFromCache(params, callback, fileInfo);
} else {
logger.info('Storage load error', err);
callback(err);
}
} else {
logger.info('Open file from content loaded from storage');
params.fileData = data;
params.rev = stat && stat.rev || null;
that.openFileWithData(params, callback, fileInfo, data, true);
}
});
};
var cacheRev = fileInfo && fileInfo.get('rev') || null;
if (cacheRev && storage.stat) {
logger.info('Stat file');
storage.stat(params.path, function(err, stat) {
if (fileInfo && (err || stat && stat.rev === cacheRev)) {
logger.info('Open file from cache because ' + (err ? 'stat error' : 'it is latest'), err);
that.openFileFromCache(params, callback, fileInfo);
} else if (stat) {
logger.info('Open file from storage');
storageLoad();
} else {
logger.info('Stat error', err);
callback(err);
}
});
} else {
storageLoad();
}
}
},
openFileFromCache: function(params, callback, fileInfo) {
var that = this;
Storage.cache.load(fileInfo.id, function(err, data) {
new Logger('open', params.name).info('Loaded file from cache', err);
if (err) {
callback(err);
} else {
that.openFileWithData(params, callback, fileInfo, data);
}
});
},
openFileWithData: function(params, callback, fileInfo, data, updateCacheOnSuccess) {
var logger = new Logger('open', params.name);
var file = new FileModel({
name: params.name,
storage: params.storage,
path: params.path,
keyFileName: params.keyFileName
});
var that = this;
file.open(params.password, data, params.keyFileData, function(err) {
if (err) {
return callback(err);
}
if (that.files.get(file.id)) {
return callback('Duplicate file id');
}
if (fileInfo && fileInfo.get('modified')) {
if (fileInfo.get('editState')) {
logger.info('Loaded local edit state');
file.setLocalEditState(fileInfo.get('editState'));
}
logger.info('Mark file as modified and schedule sync');
file.set('modified', true);
setTimeout(that.syncFile.bind(that, file), 0);
}
if (fileInfo) {
file.set('syncDate', fileInfo.get('syncDate'));
}
var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid();
file.set('cacheId', cacheId);
if (updateCacheOnSuccess && params.storage !== 'file') {
logger.info('Save loaded file to cache');
Storage.cache.save(cacheId, params.fileData);
}
var rev = params.rev || fileInfo && fileInfo.get('rev');
that.addToLastOpenFiles(file, rev);
that.addFile(file);
});
},
addToLastOpenFiles: function(file, rev) {
this.appLogger.debug('Add last open file', file.get('cacheId'), file.get('name'), file.get('storage'), file.get('path'), rev);
var dt = new Date();
var fileInfo = new FileInfoModel({
id: file.get('cacheId'),
name: file.get('name'),
storage: file.get('storage'),
path: file.get('path'),
modified: file.get('modified'),
editState: file.getLocalEditState(),
rev: rev,
syncDate: file.get('syncDate') || dt,
openDate: dt
});
this.fileInfos.remove(file.get('cacheId'));
this.fileInfos.unshift(fileInfo);
this.fileInfos.save();
},
removeFileInfo: function(id) {
Storage.cache.remove(id);
this.fileInfos.remove(id);
this.fileInfos.save();
},
syncFile: function(file, options, callback) {
var that = this;
if (file.get('demo')) {
return callback && callback();
}
if (file.get('syncing')) {
return callback && callback('Sync in progress');
}
if (!options) {
options = {};
}
var logger = new Logger('sync', file.get('name'));
var storage = options.storage || file.get('storage');
var path = options.path || file.get('path');
if (storage && Storage[storage].getPathForName && !options.path) {
path = Storage[storage].getPathForName(file.get('name'));
}
logger.info('Sync started', storage, path, options);
var fileInfo = file.get('cacheId') ? this.fileInfos.get(file.get('cacheId')) :
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path'));
if (!fileInfo) {
logger.info('Create new file info');
var dt = new Date();
fileInfo = new FileInfoModel({
id: IdGenerator.uuid(),
name: file.get('name'),
storage: file.get('storage'),
path: file.get('path'),
modified: file.get('modified'),
editState: null,
rev: null,
syncDate: dt,
openDate: dt
});
}
file.setSyncProgress();
var complete = function(err, savedToCache) {
if (!err) { savedToCache = true; }
logger.info('Sync finished', err);
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
file.set('cacheId', fileInfo.id);
fileInfo.set({
name: file.get('name'),
storage: storage,
path: path,
modified: file.get('modified'),
editState: file.getLocalEditState(),
syncDate: file.get('syncDate'),
cacheId: fileInfo.id
});
if (!that.fileInfos.get(fileInfo.id)) {
that.fileInfos.unshift(fileInfo);
}
that.fileInfos.save();
if (callback) { callback(err); }
};
if (!storage) {
if (!file.get('modified') && fileInfo.id === file.get('cacheId')) {
logger.info('Local, not modified');
return complete();
}
logger.info('Local, save to cache');
file.getData(function(data, err) {
if (err) { return complete(err); }
Storage.cache.save(fileInfo.id, data, function(err) {
logger.info('Saved to cache', err);
complete(err);
});
});
} else {
var maxLoadLoops = 3, loadLoops = 0;
var loadFromStorageAndMerge = function() {
if (++loadLoops === maxLoadLoops) {
return complete('Too many load attempts');
}
logger.info('Load from storage, attempt ' + loadLoops);
Storage[storage].load(path, function(err, data, stat) {
logger.info('Load from storage', stat, err);
if (err) { return complete(err); }
file.mergeOrUpdate(data, function(err) {
logger.info('Merge complete', err);
that.refresh();
if (err) { return complete(err); }
if (stat && stat.rev) {
logger.info('Update rev in file info');
fileInfo.set('rev', stat.rev);
}
file.set('syncDate', new Date());
if (file.get('modified')) {
logger.info('Updated sync date, saving modified file to cache and storage');
saveToCacheAndStorage();
} else if (file.get('dirty') && storage !== 'file') {
logger.info('Saving not modified dirty file to cache');
Storage.cache.save(fileInfo.id, data, function (err) {
if (err) { return complete(err); }
file.set('dirty', false);
logger.info('Complete, remove dirty flag');
complete();
});
} else {
logger.info('Complete, no changes');
complete();
}
});
});
};
var saveToCacheAndStorage = function() {
logger.info('Save to cache and storage');
file.getData(function(data, err) {
if (err) { return complete(err); }
if (!file.get('dirty') || storage === 'file') {
logger.info('Save to storage, skip cache because not dirty or file storage');
saveToStorage(data);
} else {
logger.info('Saving to cache');
Storage.cache.save(fileInfo.id, data, function (err) {
if (err) { return complete(err); }
file.set('dirty', false);
logger.info('Saved to cache, saving to storage');
saveToStorage(data);
});
}
});
};
var saveToStorage = function(data) {
logger.info('Save data to storage');
Storage[storage].save(path, data, function(err, stat) {
if (err && err.revConflict) {
logger.info('Save rev conflict, reloading from storage');
loadFromStorageAndMerge();
} else if (err) {
logger.info('Error saving data to storage');
complete(err);
} else {
if (storage === 'file') {
Storage.cache.remove(fileInfo.id);
}
if (stat && stat.rev) {
logger.info('Update rev in file info');
fileInfo.set('rev', stat.rev);
}
file.set('syncDate', new Date());
logger.info('Save to storage complete, update sync date');
complete();
}
}, fileInfo.get('rev'));
};
if (options.reload) {
logger.info('Saved to cache');
loadFromStorageAndMerge();
} else if (storage === 'file') {
if (file.get('modified') || file.get('path') !== path) {
logger.info('Save modified file to storage');
saveToCacheAndStorage();
} else {
logger.info('Skip not modified file');
complete();
}
} else {
logger.info('Stat file');
Storage[storage].stat(path, function (err, stat) {
if (err) {
if (err.notFound) {
logger.info('File does not exist in storage, creating');
saveToCacheAndStorage();
} else if (file.get('dirty')) {
logger.info('Stat error, dirty, save to cache', err);
file.getData(function (data) {
if (data) {
Storage.cache.save(fileInfo.id, data, function (e) {
if (!e) {
file.set('dirty', false);
}
logger.info('Saved to cache, exit with error', err);
complete(err);
});
}
});
} else {
logger.info('Stat error, not dirty', err);
complete(err);
}
} else if (stat.rev === fileInfo.get('rev')) {
if (file.get('modified')) {
logger.info('Stat found same version, modified, saving to cache and storage');
saveToCacheAndStorage();
} else {
logger.info('Stat found same version, not modified');
complete();
}
} else {
logger.info('Found new version, loading from storage');
loadFromStorageAndMerge();
}
});
}
}
}
});

View File

@ -1,13 +1,11 @@
'use strict';
var Backbone = require('backbone'),
Launcher = require('../comp/launcher');
var FileName = 'app-settings.json';
SettingsStore = require('../comp/settings-store');
var AppSettingsModel = Backbone.Model.extend({
defaults: {
theme: 'd',
theme: 'fb',
expandGroups: true,
listViewWidth: null,
menuViewWidth: null,
@ -17,7 +15,9 @@ var AppSettingsModel = Backbone.Model.extend({
autoSave: true,
idleMinutes: 15,
minimizeOnClose: false,
tableView: false
tableView: false,
colorfulIcons: false,
lockOnMinimize: true
},
initialize: function() {
@ -25,34 +25,15 @@ var AppSettingsModel = Backbone.Model.extend({
},
load: function() {
try {
var data;
if (Launcher) {
var settingsFile = Launcher.getUserDataPath(FileName);
if (Launcher.fileExists(settingsFile)) {
data = JSON.parse(Launcher.readFile(settingsFile, 'utf8'));
}
} else if (typeof localStorage !== 'undefined' && localStorage.appSettings) {
data = JSON.parse(localStorage.appSettings);
}
if (data) {
this.set(data, {silent: true});
}
} catch (e) {
console.error('Error loading settings', e);
var data = SettingsStore.load('app-settings');
if (data) {
if (data.theme === 'd') { data.theme = 'db'; } // TODO: remove in v0.6
this.set(data, {silent: true});
}
},
save: function() {
try {
if (Launcher) {
Launcher.writeFile(Launcher.getUserDataPath(FileName), JSON.stringify(this.attributes));
} else if (typeof localStorage !== 'undefined') {
localStorage.appSettings = JSON.stringify(this.attributes);
}
} catch (e) {
console.error('Error saving settings', e);
}
SettingsStore.save('app-settings', this.attributes);
}
});

View File

@ -16,16 +16,19 @@ var EntryModel = Backbone.Model.extend({
},
setEntry: function(entry, group, file) {
this.set({ id: entry.uuid.id }, {silent: true});
this.entry = entry;
this.group = group;
this.file = file;
if (this.id === entry.uuid.id) {
this._checkUpdatedEntry();
}
this._fillByEntry();
},
_fillByEntry: function() {
var entry = this.entry;
this.fileName = this.file.db.meta.name;
this.set({id: entry.uuid.id}, {silent: true});
this.fileName = this.file.get('name');
this.title = entry.fields.Title || '';
this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
this.notes = entry.fields.Notes || '';
@ -48,6 +51,15 @@ var EntryModel = Backbone.Model.extend({
this._buildSearchColor();
},
_checkUpdatedEntry: function() {
if (this.isJustCreated) {
this.isJustCreated = false;
}
if (this.unsaved && +this.updated !== +this.entry.times.lastModTime) {
this.unsaved = false;
}
},
_buildSearchText: function() {
var text = '';
_.forEach(this.entry.fields, function(value) {
@ -114,7 +126,8 @@ var EntryModel = Backbone.Model.extend({
},
matches: function(filter) {
return (!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
return !filter ||
(!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
(!filter.textLower || this.searchText.indexOf(filter.textLower) >= 0) &&
(!filter.color || filter.color === true && this.searchColor || this.searchColor === filter.color);
},
@ -190,7 +203,7 @@ var EntryModel = Backbone.Model.extend({
deleteHistory: function(historyEntry) {
var ix = this.entry.history.indexOf(historyEntry);
if (ix >= 0) {
this.entry.history.splice(ix, 1);
this.entry.removeHistory(ix);
}
this._fillByEntry();
},
@ -211,9 +224,10 @@ var EntryModel = Backbone.Model.extend({
},
discardUnsaved: function() {
if (this.unsaved) {
if (this.unsaved && this.entry.history.length) {
this.unsaved = false;
var historyEntry = this.entry.history.pop();
var historyEntry = this.entry.history[this.entry.history.length - 1];
this.entry.removeHistory(this.entry.history.length - 1);
this.entry.fields = {};
this.entry.binaries = {};
this.entry.copyFrom(historyEntry);
@ -227,18 +241,13 @@ var EntryModel = Backbone.Model.extend({
this.isJustCreated = false;
}
this.file.db.remove(this.entry);
this.group.removeEntry(this);
var trashGroup = this.file.getTrashGroup();
if (trashGroup) {
trashGroup.addEntry(this);
this.group = trashGroup;
}
this.file.reload();
},
deleteFromTrash: function() {
this.file.setModified();
this.file.db.move(this.entry, null);
this.group.removeEntry(this);
this.file.reload();
},
removeWithoutHistory: function() {
@ -246,7 +255,7 @@ var EntryModel = Backbone.Model.extend({
if (ix >= 0) {
this.group.group.entries.splice(ix, 1);
}
this.group.removeEntry(this);
this.file.reload();
}
});

View File

@ -0,0 +1,27 @@
'use strict';
var Backbone = require('backbone');
var FileInfoModel = Backbone.Model.extend({
defaults: {
id: '',
name: '',
storage: null,
path: null,
modified: false,
editState: null,
rev: null,
syncDate: null,
openDate: null
},
initialize: function(data, options) {
_.each(data, function(val, key) {
if (/Date$/.test(key)) {
this.set(key, val ? new Date(val) : null, options);
}
}, this);
}
});
module.exports = FileInfoModel;

View File

@ -3,25 +3,24 @@
var Backbone = require('backbone'),
GroupCollection = require('../collections/group-collection'),
GroupModel = require('./group-model'),
Launcher = require('../comp/launcher'),
DropboxLink = require('../comp/dropbox-link'),
Storage = require('../comp/storage'),
LastOpenFiles = require('../comp/last-open-files'),
IconUrl = require('../util/icon-url'),
Logger = require('../util/logger'),
kdbxweb = require('kdbxweb'),
demoFileData = require('base64!../../resources/Demo.kdbx');
var logger = new Logger('file');
var FileModel = Backbone.Model.extend({
defaults: {
id: '',
name: '',
keyFileName: '',
passwordLength: 0,
path: '',
storage: null,
modified: false,
dirty: false,
open: false,
opening: false,
error: false,
created: false,
demo: false,
groups: null,
@ -30,17 +29,21 @@ var FileModel = Backbone.Model.extend({
passwordChanged: false,
keyFileChanged: false,
syncing: false,
availOffline: false,
offline: false
syncError: null,
syncDate: null,
cacheId: null
},
db: null,
data: null,
entryMap: null,
groupMap: null,
initialize: function() {
this.entryMap = {};
this.groupMap = {};
},
open: function(password, fileData, keyFileData) {
open: function(password, fileData, keyFileData, callback) {
var len = password.value.length,
byteLength = 0,
value = new Uint8Array(len * 4),
@ -57,80 +60,54 @@ var FileModel = Backbone.Model.extend({
password = new kdbxweb.ProtectedValue(value.buffer.slice(0, byteLength), salt.buffer.slice(0, byteLength));
try {
var credentials = new kdbxweb.Credentials(password, keyFileData);
var start = performance.now();
var ts = logger.ts();
kdbxweb.Kdbx.load(fileData, credentials, (function(db, err) {
if (err) {
this.set({error: true, opening: false});
console.error('Error opening file', err.code, err.message, err);
logger.error('Error opening file', err.code, err.message, err);
callback(err);
} else {
this.db = db;
this.readModel(this.get('name'));
this.readModel();
this.setOpenFile({ passwordLength: len });
if (keyFileData) {
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
}
console.log('Opened file ' + this.get('name') + ': ' + Math.round(performance.now() - start) + 'ms, ' +
logger.info('Opened file ' + this.get('name') + ': ' + logger.ts(ts) + ', ' +
db.header.keyEncryptionRounds + ' rounds, ' + Math.round(fileData.byteLength / 1024) + ' kB');
this.postOpen(fileData);
callback();
}
}).bind(this));
} catch (e) {
console.error('Error opening file', e, e.code, e.message, e);
this.set({ error: true, opening: false });
logger.error('Error opening file', e, e.code, e.message, e);
callback(e);
}
},
postOpen: function(fileData) {
var that = this;
this.data = fileData;
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.set('name', name);
this.readModel();
this.set({ open: true, created: true, opening: false, error: false, name: name, offline: false });
this.set({ open: true, created: true, name: name });
},
createDemo: function() {
openDemo: function(callback) {
var password = kdbxweb.ProtectedValue.fromString('demo');
var credentials = new kdbxweb.Credentials(password);
var demoFile = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(demoFileData));
kdbxweb.Kdbx.load(demoFile, credentials, (function(db) {
this.db = db;
this.set('name', 'Demo');
this.readModel();
this.setOpenFile({passwordLength: 4, demo: true, name: 'Demo'});
this.setOpenFile({passwordLength: 4, demo: true});
callback();
}).bind(this));
},
setOpenFile: function(props) {
_.extend(props, {
open: true,
opening: false,
error: false,
oldKeyFileName: this.get('keyFileName'),
oldPasswordLength: props.passwordLength,
passwordChanged: false,
@ -142,9 +119,10 @@ var FileModel = Backbone.Model.extend({
this._oldKeyChangeDate = this.db.meta.keyChanged;
},
readModel: function(topGroupTitle) {
readModel: function() {
var groups = new GroupCollection();
this.set({
id: this.db.getDefaultGroup().uuid.toString(),
groups: groups,
defaultUser: this.db.meta.defaultUser,
recycleBinEnabled: this.db.meta.recycleBinEnabled,
@ -152,13 +130,66 @@ var FileModel = Backbone.Model.extend({
historyMaxSize: this.db.meta.historyMaxSize,
keyEncryptionRounds: this.db.header.keyEncryptionRounds
}, { silent: true });
this.db.groups.forEach(function(group, index) {
var groupModel = GroupModel.fromGroup(group, this);
if (index === 0 && topGroupTitle) {
groupModel.set({title: topGroupTitle});
this.db.groups.forEach(function(group) {
var groupModel = this.getGroup(group.uuid.id);
if (groupModel) {
groupModel.setGroup(group, this);
} else {
groupModel = GroupModel.fromGroup(group, this);
}
groupModel.set({title: this.get('name')});
groups.add(groupModel);
}, this);
this.buildObjectMap();
},
buildObjectMap: function() {
var entryMap = {};
var groupMap = {};
this.forEachGroup(function(group) {
groupMap[group.id] = group;
group.forEachOwnEntry(null, function(entry) {
entryMap[entry.id] = entry;
});
}, true);
this.entryMap = entryMap;
this.groupMap = groupMap;
},
reload: function() {
this.buildObjectMap();
this.readModel();
this.trigger('reload', this);
},
mergeOrUpdate: function(fileData, callback) {
kdbxweb.Kdbx.load(fileData, this.db.credentials, (function(remoteDb, err) {
if (err) {
logger.error('Error opening file to merge', err.code, err.message, err);
} else {
if (this.get('modified')) {
try {
this.db.merge(remoteDb);
} catch (e) {
logger.error('File merge error', e);
return callback(e);
}
} else {
this.db = remoteDb;
}
this.set('dirty', true);
this.reload();
}
callback(err);
}).bind(this));
},
getLocalEditState: function() {
return this.db.getLocalEditState();
},
setLocalEditState: function(editState) {
this.db.setLocalEditState(editState);
},
close: function() {
@ -166,9 +197,8 @@ var FileModel = Backbone.Model.extend({
keyFileName: '',
passwordLength: 0,
modified: false,
dirty: false,
open: false,
opening: false,
error: false,
created: false,
groups: null,
passwordChanged: false,
@ -177,17 +207,12 @@ var FileModel = Backbone.Model.extend({
});
},
getEntry: function(id) {
return this.entryMap[id];
},
getGroup: function(id) {
var found = null;
if (id) {
this.forEachGroup(function (group) {
if (group.get('id') === id) {
found = group;
return false;
}
}, true);
}
return found;
return this.groupMap[id];
},
forEachEntry: function(filter, callback) {
@ -223,35 +248,7 @@ var FileModel = Backbone.Model.extend({
setModified: function() {
if (!this.get('demo')) {
this.set('modified', true);
}
},
autoSave: function(complete) {
var that = this;
that.set('syncing', true);
switch (that.get('storage')) {
case 'file':
that.getData(function(data) {
Launcher.writeFile(that.get('path'), data);
that.saved(that.get('path'), that.get('storage'));
if (complete) { complete(); }
});
break;
case 'dropbox':
that.getData(function(data) {
DropboxLink.saveFile(that.get('path'), data, true, function (err) {
if (err) {
that.set('syncing', false);
} else {
that.saved(that.get('path'), that.get('storage'));
}
if (complete) { complete(err); }
});
});
break;
default:
throw 'Unknown storage; cannot auto save';
this.set({ modified: true, dirty: true });
}
},
@ -260,21 +257,42 @@ var FileModel = Backbone.Model.extend({
historyRules: true,
customIcons: true
});
this.data = this.db.save(cb);
return this.data;
var that = this;
this.db.save(function(data, err) {
if (err) {
logger.error('Error saving file', that.get('name'), err);
}
cb(data, err);
});
},
getXml: function(cb) {
this.db.saveXml(cb);
},
saved: function(path, storage) {
this.set({ path: path || '', storage: storage || null, modified: false, created: false, syncing: false });
setSyncProgress: function() {
this.set({ syncing: true });
},
setSyncComplete: function(path, storage, error, savedToCache) {
if (!error) {
this.db.removeLocalEditState();
}
var modified = this.get('modified') && !!error;
var dirty = this.get('dirty') && !savedToCache;
this.set({
created: false,
path: path || this.get('path'),
storage: storage || this.get('storage'),
modified: modified,
dirty: dirty,
syncing: false,
syncError: error
});
this.setOpenFile({ passwordLength: this.get('passwordLength') });
this.forEachEntry({}, function(entry) {
entry.unsaved = false;
});
this.addToLastOpenFiles();
},
setPassword: function(password) {
@ -329,6 +347,7 @@ var FileModel = Backbone.Model.extend({
this.set('name', name);
this.get('groups').first().setName(name);
this.setModified();
this.reload();
},
setDefaultUser: function(defaultUser) {

View File

@ -16,34 +16,51 @@ var GroupModel = MenuItemModel.extend({
editable: true,
top: false,
drag: true,
drop: true
drop: true,
enableSearching: true
}),
initialize: function() {
if (!GroupCollection) { GroupCollection = require('../collections/group-collection'); }
if (!EntryCollection) { EntryCollection = require('../collections/entry-collection'); }
this.set('entries', new EntryCollection());
},
setFromGroup: function(group, file) {
setGroup: function(group, file, parentGroup) {
var isRecycleBin = file.db.meta.recycleBinUuid && file.db.meta.recycleBinUuid.id === group.uuid.id;
this.set({
id: group.uuid.id,
expanded: true,
expanded: group.expanded,
visible: !isRecycleBin,
items: new GroupCollection(),
filterValue: group.uuid.id
entries: new EntryCollection(),
filterValue: group.uuid.id,
enableSearching: group.enableSearching,
top: !parentGroup,
drag: !!parentGroup
}, { silent: true });
this.group = group;
this.file = file;
this.parentGroup = parentGroup;
this._fillByGroup(true);
var items = this.get('items'),
entries = this.get('entries');
group.groups.forEach(function(subGroup) {
items.add(GroupModel.fromGroup(subGroup, file, this));
var existing = file.getGroup(subGroup.uuid);
if (existing) {
existing.setGroup(subGroup, file, this);
items.add(existing);
} else {
items.add(GroupModel.fromGroup(subGroup, file, this));
}
}, this);
group.entries.forEach(function(entry) {
entries.add(EntryModel.fromEntry(entry, this, file));
var existing = file.getEntry(entry.uuid);
if (existing) {
existing.setEntry(entry, this, file);
entries.add(existing);
} else {
entries.add(EntryModel.fromEntry(entry, this, file));
}
}, this);
},
@ -102,22 +119,12 @@ var GroupModel = MenuItemModel.extend({
return this.group.groups;
},
removeEntry: function(entry) {
this.get('entries').remove(entry);
},
addEntry: function(entry) {
this.get('entries').add(entry);
},
removeGroup: function(group) {
this.get('items').remove(group);
this.trigger('remove', group);
},
addGroup: function(group) {
this.get('items').add(group);
this.trigger('insert', group);
},
setName: function(name) {
@ -139,21 +146,27 @@ var GroupModel = MenuItemModel.extend({
this._fillByGroup();
},
setExpanded: function(expanded) {
this._groupModified();
this.group.expanded = expanded;
this.set('expanded', expanded);
},
setEnableSearching: function(enabled) {
this._groupModified();
this.group.enableSearching = enabled;
this.set('enableSearching', enabled);
},
moveToTrash: function() {
this.file.setModified();
this.file.db.remove(this.group);
this.parentGroup.removeGroup(this);
var trashGroup = this.file.getTrashGroup();
if (trashGroup) {
trashGroup.addGroup(this);
this.parentGroup = trashGroup;
}
this.trigger('delete');
this.file.reload();
},
deleteFromTrash: function() {
this.file.db.move(this.group, null);
this.parentGroup.removeGroup(this);
this.file.reload();
},
removeWithoutHistory: function() {
@ -161,8 +174,7 @@ var GroupModel = MenuItemModel.extend({
if (ix >= 0) {
this.parentGroup.group.groups.splice(ix, 1);
}
this.parentGroup.removeGroup(this);
this.trigger('delete');
this.file.reload();
},
moveHere: function(object) {
@ -175,42 +187,32 @@ var GroupModel = MenuItemModel.extend({
return;
}
this.file.db.move(object.group, this.group);
object.parentGroup.removeGroup(object);
object.trigger('delete');
this.addGroup(object);
this.file.reload();
} else if (object instanceof EntryModel) {
if (this.group.entries.indexOf(object.entry) >= 0) {
return;
}
this.file.db.move(object.entry, this.group);
object.group.removeEntry(object);
this.addEntry(object);
object.group = this;
this.file.reload();
}
}
});
GroupModel.fromGroup = function(group, file, parentGroup) {
var model = new GroupModel();
model.setFromGroup(group, file);
if (parentGroup) {
model.parentGroup = parentGroup;
} else {
model.set({ top: true, drag: false }, { silent: true });
}
model.setGroup(group, file, parentGroup);
return model;
};
GroupModel.newGroup = function(group, file) {
var model = new GroupModel();
var grp = file.db.createGroup(group.group);
model.setFromGroup(grp, file);
model.setGroup(grp, file, group);
model.group.times.update();
model.parentGroup = group;
model.unsaved = true;
model.isJustCreated = true;
group.addGroup(model);
file.setModified();
file.reload();
return model;
};

View File

@ -27,7 +27,7 @@ var MenuItemModel = Backbone.Model.extend({
this.trigger('change-items');
},
removeByFile: function(file) {
removeByFile: function(file, skipEvent) {
var items = this.get('items');
var toRemove;
items.each(function(item) {
@ -38,7 +38,9 @@ var MenuItemModel = Backbone.Model.extend({
if (toRemove) {
items.remove(toRemove);
}
this.trigger('change-items');
if (!skipEvent) {
this.trigger('change-items');
}
},
setItems: function(items) {

View File

@ -1,6 +1,7 @@
'use strict';
var Backbone = require('backbone');
var Backbone = require('backbone'),
SettingsStore = require('../comp/settings-store');
var UpdateModel = Backbone.Model.extend({
defaults: {
@ -19,9 +20,9 @@ var UpdateModel = Backbone.Model.extend({
},
load: function() {
if (localStorage.updateInfo) {
var data = SettingsStore.load('update-info');
if (data) {
try {
var data = JSON.parse(localStorage.updateInfo);
_.each(data, function(val, key) {
if (/Date$/.test(key)) {
data[key] = val ? new Date(val) : null;
@ -39,7 +40,7 @@ var UpdateModel = Backbone.Model.extend({
delete attr[key];
}
});
localStorage.updateInfo = JSON.stringify(attr);
SettingsStore.save('update-info', attr);
}
});

View File

@ -2,9 +2,11 @@
var Format = require('../util/format');
var EntryPresenter = function(descField) {
var EntryPresenter = function(descField, noColor, activeEntryId) {
this.entry = null;
this.descField = descField;
this.noColor = noColor || '';
this.activeEntryId = activeEntryId;
};
EntryPresenter.prototype = {
@ -19,12 +21,12 @@ EntryPresenter.prototype = {
get id() { return this.entry ? this.entry.id : this.group.get('id'); },
get icon() { return this.entry ? this.entry.icon : (this.group.get('icon') || 'folder'); },
get customIcon() { return this.entry ? this.entry.customIcon : undefined; },
get color() { return this.entry ? this.entry.color : undefined; },
get color() { return this.entry ? (this.entry.color || (this.entry.customIcon ? this.noColor : undefined)) : undefined; },
get title() { return this.entry ? this.entry.title : this.group.get('title'); },
get notes() { return this.entry ? this.entry.notes : undefined; },
get url() { return this.entry ? this.entry.url : undefined; },
get user() { return this.entry ? this.entry.user : undefined; },
get active() { return this.entry ? this.entry.active : this.group.active; },
get active() { return this.entry ? this.entry.id === this.activeEntryId : this.group.active; },
get created() { return this.entry ? Format.dtStr(this.entry.created) : undefined; },
get updated() { return this.entry ? Format.dtStr(this.entry.updated) : undefined; },
get expired() { return this.entry ? this.entry.expired : false; },

View File

@ -0,0 +1,11 @@
'use strict';
var Launcher = require('../comp/launcher');
var Storage = {
file: require('./storage-file'),
dropbox: require('./storage-dropbox'),
cache: Launcher ? require('./storage-file-cache') : require('./storage-cache')
};
module.exports = Storage;

View File

@ -1,5 +1,8 @@
'use strict';
var Logger = require('../util/logger');
var logger = new Logger('storage-cache');
var idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
var StorageCache = {
@ -11,101 +14,98 @@ var StorageCache = {
init: function(callback) {
if (this.db) {
return callback();
return callback && callback();
}
var that = this;
try {
var req = idb.open('FilesCache');
req.onerror = function (e) {
console.error('Error opening indexed db', e);
logger.error('Error opening indexed db', e);
that.errorOpening = e;
callback(e);
if (callback) { callback(e); }
};
req.onsuccess = function (e) {
that.db = e.target.result;
callback();
if (callback) { callback(); }
};
req.onupgradeneeded = function (e) {
var db = e.target.result;
db.createObjectStore('files');
};
} catch (e) {
console.error('Error opening indexed db', e);
callback(e);
logger.error('Error opening indexed db', e);
if (callback) { callback(e); }
}
},
save: function(id, data, callback) {
logger.debug('Save', id);
this.init((function(err) {
if (err) {
return callback(err);
return callback && callback(err);
}
try {
var ts = logger.ts();
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
req.onsuccess = function () {
if (callback) {
callback();
}
logger.debug('Saved', id, logger.ts(ts));
if (callback) { callback(); }
};
req.onerror = function () {
console.error('Error saving to cache', id, req.error);
if (callback) {
callback(req.error);
}
logger.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);
logger.error('Error saving to cache', id, e);
if (callback) { callback(e); }
}
}).bind(this));
},
load: function(id, callback) {
logger.debug('Load', id);
this.init((function(err) {
if (err) {
return callback(null, err);
return callback && callback(err, null);
}
try {
var ts = logger.ts();
var req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
req.onsuccess = function () {
if (callback) {
callback(req.result);
}
logger.debug('Loaded', id, logger.ts(ts));
if (callback) { callback(null, req.result); }
};
req.onerror = function () {
console.error('Error loading from cache', id, req.error);
if (callback) {
callback(null, req.error);
}
logger.error('Error loading from cache', id, req.error);
if (callback) { callback(req.error); }
};
} catch (e) {
console.error('Error loading from cache', id, e);
callback(null, e);
logger.error('Error loading from cache', id, e);
if (callback) { callback(e, null); }
}
}).bind(this));
},
remove: function(id, callback) {
logger.debug('Remove', id);
this.init((function(err) {
if (err) {
return callback(err);
return callback && callback(err);
}
try {
var ts = logger.ts();
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
req.onsuccess = function () {
if (callback) {
callback();
}
logger.debug('Removed', id, logger.ts(ts));
if (callback) { callback(); }
};
req.onerror = function () {
console.error('Error removing from cache', id, req.error);
if (callback) {
callback(req.error);
}
logger.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);
logger.error('Error removing from cache', id, e);
if (callback) { callback(e); }
}
}).bind(this));
}

View File

@ -0,0 +1,65 @@
'use strict';
var DropboxLink = require('../comp/dropbox-link'),
Logger = require('../util/logger');
var logger = new Logger('storage-dropbox');
var StorageDropbox = {
name: 'dropbox',
enabled: true,
_convertError: function(err) {
if (!err) {
return err;
}
if (err.status === DropboxLink.ERROR_NOT_FOUND) {
err.notFound = true;
}
if (err.status === DropboxLink.ERROR_CONFLICT) {
err.revConflict = true;
}
return err;
},
getPathForName: function(fileName) {
return '/' + fileName + '.kdbx';
},
load: function(path, callback) {
logger.debug('Load', path);
var ts = logger.ts();
DropboxLink.openFile(path, function(err, data, stat) {
logger.debug('Loaded', path, stat ? stat.versionTag : null, logger.ts(ts));
err = StorageDropbox._convertError(err);
if (callback) { callback(err, data, stat ? { rev: stat.versionTag } : null); }
}, _.noop);
},
stat: function(path, callback) {
logger.debug('Stat', path);
var ts = logger.ts();
DropboxLink.stat(path, function(err, stat) {
if (stat && stat.isRemoved) {
err = new Error('File removed');
err.notFound = true;
}
logger.debug('Stated', path, stat ? stat.versionTag : null, logger.ts(ts));
err = StorageDropbox._convertError(err);
if (callback) { callback(err, stat ? { rev: stat.versionTag } : null); }
}, _.noop);
},
save: function(path, data, callback, rev) {
logger.debug('Save', path, rev);
var ts = logger.ts();
DropboxLink.saveFile(path, data, rev, function(err, stat) {
logger.debug('Saved', path, logger.ts(ts));
if (!callback) { return; }
err = StorageDropbox._convertError(err);
callback(err, stat ? { rev: stat.versionTag } : null);
}, _.noop);
}
};
module.exports = StorageDropbox;

View File

@ -0,0 +1,94 @@
'use strict';
var Launcher = require('../comp/launcher'),
Logger = require('../util/logger');
var logger = new Logger('storage-file-cache');
var StorageFileCache = {
name: 'cache',
enabled: !!Launcher,
path: null,
getPath: function(id) {
return Launcher.req('path').join(this.path, id);
},
init: function(callback) {
if (this.path) {
return callback && callback();
}
try {
var path = Launcher.getUserDataPath('OfflineFiles');
var fs = Launcher.req('fs');
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
this.path = path;
callback();
} catch (e) {
logger.error('Error opening local offline storage', e);
if (callback) { callback(e); }
}
},
save: function(id, data, callback) {
logger.debug('Save', id);
this.init((function(err) {
if (err) {
return callback && callback(err);
}
var ts = logger.ts();
try {
Launcher.writeFile(this.getPath(id), data);
logger.debug('Saved', id, logger.ts(ts));
if (callback) { callback(); }
} catch (e) {
logger.error('Error saving to cache', id, e);
if (callback) { callback(e); }
}
}).bind(this));
},
load: function(id, callback) {
logger.debug('Load', id);
this.init((function(err) {
if (err) {
return callback && callback(null, err);
}
var ts = logger.ts();
try {
var data = Launcher.readFile(this.getPath(id));
logger.debug('Loaded', id, logger.ts(ts));
if (callback) { callback(null, data.buffer); }
} catch (e) {
logger.error('Error loading from cache', id, e);
if (callback) { callback(e, null); }
}
}).bind(this));
},
remove: function(id, callback) {
logger.debug('Remove', id);
this.init((function(err) {
if (err) {
return callback && callback(err);
}
var ts = logger.ts();
try {
var path = this.getPath(id);
if (Launcher.fileExists(path)) {
Launcher.deleteFile(path);
}
logger.debug('Removed', id, logger.ts(ts));
if (callback) { callback(); }
} catch(e) {
logger.error('Error removing from cache', id, e);
if (callback) { callback(e); }
}
}).bind(this));
}
};
module.exports = StorageFileCache;

View File

@ -0,0 +1,39 @@
'use strict';
var Launcher = require('../comp/launcher'),
Logger = require('../util/logger');
var logger = new Logger('storage-file');
var StorageFile = {
name: 'file',
enabled: !!Launcher,
load: function(path, callback) {
logger.debug('Load', path);
var ts = logger.ts();
try {
var data = Launcher.readFile(path);
logger.debug('Loaded', path, logger.ts(ts));
if (callback) { callback(null, data.buffer); }
} catch (e) {
logger.error('Error reading local file', path, e);
if (callback) { callback(e, null); }
}
},
save: function(path, data, callback) {
logger.debug('Save', path);
var ts = logger.ts();
try {
Launcher.writeFile(path, data);
logger.debug('Saved', path, logger.ts(ts));
if (callback) { callback(); }
} catch (e) {
logger.error('Error writing local file', path, e);
if (callback) { callback(e); }
}
}
};
module.exports = StorageFile;

View File

@ -0,0 +1,13 @@
'use strict';
var IdGenerator = {
uuid: function() {
var s4 = IdGenerator.s4;
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
},
s4: function() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
};
module.exports = IdGenerator;

View File

@ -0,0 +1,42 @@
'use strict';
/* globals console */
/* globals performance */
var Logger = function(name, id) {
this.prefix = (name ? name + (id ? ':' + id : '') : 'default');
};
Logger.prototype.ts = function(ts) {
if (ts) {
return Math.round(performance.now() - ts) + 'ms';
} else {
return performance.now();
}
};
Logger.prototype.getPrefix = function() {
return new Date().toISOString() + ' [' + this.prefix + '] ';
};
Logger.prototype.debug = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.debug.apply(console, arguments);
};
Logger.prototype.info = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.log.apply(console, arguments);
};
Logger.prototype.warn = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.warn.apply(console, arguments);
};
Logger.prototype.error = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.error.apply(console, arguments);
};
module.exports = Logger;

View File

@ -0,0 +1,11 @@
'use strict';
var StringUtil = {
camelCaseRegex: /\-./g,
camelCase: function(str) {
return str.replace(this.camelCaseRegex, function(match) { return match[1].toUpperCase(); });
}
};
module.exports = StringUtil;

View File

@ -12,6 +12,7 @@ var Backbone = require('backbone'),
SettingsView = require('../views/settings/settings-view'),
Alerts = require('../comp/alerts'),
Keys = require('../const/keys'),
Timeouts = require('../const/timeouts'),
KeyHandler = require('../comp/key-handler'),
IdleTracker = require('../comp/idle-tracker'),
Launcher = require('../comp/launcher'),
@ -64,6 +65,7 @@ var AppView = Backbone.View.extend({
this.listenTo(Backbone, 'edit-group', this.editGroup);
this.listenTo(Backbone, 'launcher-open-file', this.launcherOpenFile);
this.listenTo(Backbone, 'user-idle', this.userIdle);
this.listenTo(Backbone, 'app-minimized', this.appMinimized);
this.listenTo(UpdateModel.instance, 'change:updateReady', this.updateApp);
@ -72,6 +74,8 @@ var AppView = Backbone.View.extend({
KeyHandler.onKey(Keys.DOM_VK_ESCAPE, this.escPressed, this);
KeyHandler.onKey(Keys.DOM_VK_BACK_SPACE, this.backspacePressed, this);
setInterval(this.syncAllByTimer.bind(this), Timeouts.AutoSync);
},
render: function () {
@ -85,10 +89,11 @@ var AppView = Backbone.View.extend({
this.views.listDrag.setElement(this.$el.find('.app__list-drag')).render();
this.views.details.setElement(this.$el.find('.app__details')).render();
this.views.grp.setElement(this.$el.find('.app__grp')).render().hide();
this.showLastOpenFile();
return this;
},
showOpenFile: function(filePath) {
showOpenFile: function() {
this.views.menu.hide();
this.views.menuDrag.hide();
this.views.listWrap.hide();
@ -101,15 +106,21 @@ var AppView = Backbone.View.extend({
this.hideOpenFile();
this.views.open = new OpenView({ model: this.model });
this.views.open.setElement(this.$el.find('.app__body')).render();
this.views.open.on('cancel', this.showEntries, this);
if (Launcher && filePath) {
this.views.open.showOpenLocalFile(filePath);
this.views.open.on('close', this.showEntries, this);
},
showLastOpenFile: function() {
this.showOpenFile();
var lastOpenFile = this.model.fileInfos.getLast();
if (lastOpenFile) {
this.views.open.showOpenFileInfo(lastOpenFile);
}
},
launcherOpenFile: function(path) {
if (path && /\.kdbx$/i.test(path)) {
this.showOpenFile(path);
this.showOpenFile();
this.views.open.showOpenLocalFile(path);
}
},
@ -158,7 +169,7 @@ var AppView = Backbone.View.extend({
this.views.details.hide();
this.views.grp.hide();
this.hideOpenFile();
this.views.settings = new SettingsView();
this.views.settings = new SettingsView({ model: this.model });
this.views.settings.setElement(this.$el.find('.app__body')).render();
if (!selectedMenuItem) {
selectedMenuItem = this.model.menu.generalSection.get('items').first();
@ -207,7 +218,7 @@ var AppView = Backbone.View.extend({
},
beforeUnload: function(e) {
if (this.model.files.hasUnsavedFiles()) {
if (this.model.files.hasDirtyFiles()) {
if (Launcher && !Launcher.exitRequested) {
if (!this.exitAlertShown) {
var that = this;
@ -232,7 +243,6 @@ var AppView = Backbone.View.extend({
return 'You have unsaved files, all changes will be lost.';
} else if (Launcher && !Launcher.exitRequested && !Launcher.restartPending &&
Launcher.canMinimize() && this.model.settings.get('minimizeOnClose')) {
this.lockWorkspace(true);
Launcher.minimizeApp();
return Launcher.preventExit(e);
}
@ -269,6 +279,12 @@ var AppView = Backbone.View.extend({
this.lockWorkspace(true);
},
appMinimized: function() {
if (this.model.settings.get('lockOnMinimize')) {
this.lockWorkspace(true);
}
},
lockWorkspace: function(autoInit) {
var that = this;
if (Alerts.alertDisplayed) {
@ -278,20 +294,18 @@ var AppView = Backbone.View.extend({
if (this.model.settings.get('autoSave')) {
this.saveAndLock(autoInit);
} else {
if (autoInit) {
this.showVisualLock('Auto-save is disabled. Please, enable it, to allow auto-locking');
return;
}
var message = autoInit ? 'The app cannot be locked because auto save is disabled.'
: 'You have unsaved changes that will be lost. Continue?';
Alerts.alert({
icon: 'lock',
header: 'Lock',
body: 'You have unsaved changes that will be lost. Continue?',
body: message,
buttons: [
{ result: 'save', title: 'Save changes' },
{ result: 'discard', title: 'Discard changes', error: true },
{ result: '', title: 'Cancel' }
],
checkbox: 'Auto save changes each time I lock the app',
checkbox: 'Save changes automatically',
success: function(result, autoSaveChecked) {
if (result === 'save') {
if (autoSaveChecked) {
@ -309,32 +323,16 @@ var AppView = Backbone.View.extend({
}
},
showVisualLock: function() {
// TODO: remove cases which lead to this
},
saveAndLock: function(autoInit) {
// TODO: move to file manager
saveAndLock: function(/*autoInit*/) {
var pendingCallbacks = 0,
errorFiles = [],
that = this;
if (this.model.files.some(function(file) { return file.get('modified') && !file.get('path'); })) {
this.showVisualLock('You have unsaved files, locking is not possible.');
return;
}
this.model.files.forEach(function(file) {
if (!file.get('modified')) {
if (!file.get('dirty')) {
return;
}
if (file.get('path')) {
try {
file.autoSave(fileSaved.bind(this, file));
pendingCallbacks++;
} catch (e) {
console.error('Failed to auto-save file', file.get('path'), e);
errorFiles.push(file);
}
}
this.model.syncFile(file, null, fileSaved.bind(this, file));
pendingCallbacks++;
}, this);
if (!pendingCallbacks) {
this.closeAllFilesAndShowFirst();
@ -344,10 +342,8 @@ var AppView = Backbone.View.extend({
errorFiles.push(file.get('name'));
}
if (--pendingCallbacks === 0) {
if (errorFiles.length) {
if (autoInit) {
that.showVisualLock('Failed to save files: ' + errorFiles.join(', '));
} else if (!Alerts.alertDisplayed) {
if (errorFiles.length && that.model.files.hasDirtyFiles()) {
if (!Alerts.alertDisplayed) {
Alerts.error({
header: 'Save Error',
body: 'Failed to auto-save file' + (errorFiles.length > 1 ? 's: ' : '') + ' ' + errorFiles.join(', ')
@ -364,26 +360,22 @@ var AppView = Backbone.View.extend({
var firstFile = this.model.files.find(function(file) { return !file.get('demo') && !file.get('created'); });
this.model.closeAllFiles();
if (firstFile) {
this.views.open.showClosedFile(firstFile);
var fileInfo = this.model.fileInfos.getMatch(firstFile.get('storage'), firstFile.get('name'), firstFile.get('path'));
if (fileInfo) {
this.views.open.showOpenFileInfo(fileInfo);
}
}
},
saveAll: function() {
var fileId;
this.model.files.forEach(function(file) {
if (file.get('path')) {
try {
file.autoSave();
} catch (e) {
console.error('Failed to auto-save file', file.get('path'), e);
fileId = file.cid;
}
} else if (!fileId) {
fileId = file.cid;
}
});
if (fileId) {
this.showFileSettings({fileId: fileId});
this.model.syncFile(file);
}, this);
},
syncAllByTimer: function() {
if (this.model.settings.get('autoSave')) {
this.saveAll();
}
},

View File

@ -115,7 +115,7 @@ var DetailsView = Backbone.View.extend({
value: function() { return model.notes; } } }));
this.fieldViews.push(new FieldViewTags({ model: { name: 'Tags', title: 'Tags', tags: this.appModel.tags,
value: function() { return model.tags; } } }));
this.fieldViews.push(new FieldViewDate({ model: { name: 'Expires', title: 'Expires', empty: 'Never', lessThanNow: '(expired)',
this.fieldViews.push(new FieldViewDate({ model: { name: 'Expires', title: 'Expires', lessThanNow: '(expired)',
value: function() { return model.expires; } } }));
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: 'File',
value: function() { return model.fileName; } } }));

View File

@ -7,7 +7,7 @@ var FieldViewText = require('./field-view-text'),
var FieldViewDate = FieldViewText.extend({
renderValue: function(value) {
var result = value ? Format.dStr(value) : this.model.empty || '';
var result = value ? Format.dStr(value) : '';
if (value && this.model.lessThanNow && value < new Date()) {
result += ' ' + this.model.lessThanNow;
}
@ -15,7 +15,7 @@ var FieldViewDate = FieldViewText.extend({
},
getEditValue: function(value) {
return value ? Format.dStr(value) : this.model.empty || '';
return value ? Format.dStr(value) : '';
},
startEdit: function() {

View File

@ -12,7 +12,8 @@ var GrpView = Backbone.View.extend({
'click .grp__icon': 'showIconsSelect',
'click .grp__buttons-trash': 'moveToTrash',
'click .grp__back-button': 'returnToApp',
'blur #grp__field-title': 'titleBlur'
'blur #grp__field-title': 'titleBlur',
'change #grp__check-search': 'setEnableSearching'
},
initialize: function() {
@ -26,6 +27,7 @@ var GrpView = Backbone.View.extend({
title: this.model.get('title'),
icon: this.model.get('icon') || 'folder',
customIcon: this.model.get('customIcon'),
enableSearching: this.model.get('enableSearching') !== false,
readonly: this.model.get('top')
}));
if (!this.model.get('title')) {
@ -107,6 +109,11 @@ var GrpView = Backbone.View.extend({
Backbone.trigger('select-all');
},
setEnableSearching: function(e) {
var enabled = e.target.checked;
this.model.setEnableSearching(enabled);
},
returnToApp: function() {
Backbone.trigger('edit-group');
}

View File

@ -2,7 +2,10 @@
var Backbone = require('backbone'),
IconMap = require('../const/icon-map'),
Launcher = require('../comp/launcher');
Launcher = require('../comp/launcher'),
Logger = require('../util/logger');
var logger = new Logger('icon-select-view');
var IconSelectView = Backbone.View.extend({
template: require('templates/icon-select.html'),
@ -67,7 +70,7 @@ var IconSelectView = Backbone.View.extend({
that.downloadingFavicon = false;
};
img.onerror = function (e) {
console.error('Favicon download error: ' + url, e);
logger.error('Favicon download error: ' + url, e);
that.$el.find('.icon-select__icon-download>i').removeClass('fa-spinner fa-spin');
that.$el.find('.icon-select__icon-download').removeClass('icon-select__icon--custom-selected');
that.downloadingFavicon = false;

View File

@ -66,14 +66,15 @@ var ListView = Backbone.View.extend({
if (this.items.length) {
var itemTemplate = this.getItemTemplate();
var itemsTemplate = this.getItemsTemplate();
var presenter = new EntryPresenter(this.getDescField());
var noColor = AppSettingsModel.instance.get('colorfulIcons') ? '' : 'grayscale';
var presenter = new EntryPresenter(this.getDescField(), noColor, this.model.activeEntryId);
var itemsHtml = '';
this.items.forEach(function (item) {
presenter.present(item);
itemsHtml += itemTemplate(presenter);
}, this);
var html = itemsTemplate({ items: itemsHtml });
this.itemsEl.html(html).scrollTop(0);
this.itemsEl.html(html);
} else {
this.itemsEl.html(this.emptyTemplate());
}
@ -115,16 +116,14 @@ var ListView = Backbone.View.extend({
},
selectPrev: function() {
var activeItem = this.items.getActive(),
ix = this.items.indexOf(activeItem);
var ix = this.items.indexOf(this.items.get(this.model.activeEntryId));
if (ix > 0) {
this.selectItem(this.items.at(ix - 1));
}
},
selectNext: function() {
var activeItem = this.items.getActive(),
ix = this.items.indexOf(activeItem);
var ix = this.items.indexOf(this.items.get(this.model.activeEntryId));
if (ix < this.items.length - 1) {
this.selectItem(this.items.at(ix + 1));
}
@ -143,10 +142,10 @@ var ListView = Backbone.View.extend({
},
selectItem: function(item) {
this.items.setActive(item);
this.model.activeEntryId = item.id;
Backbone.trigger('select-entry', item);
this.itemsEl.find('.list__item--active').removeClass('list__item--active');
var itemEl = document.getElementById(item.get('id'));
var itemEl = document.getElementById(item.id);
itemEl.classList.add('list__item--active');
var listEl = this.itemsEl[0],
itemRect = itemEl.getBoundingClientRect(),

View File

@ -96,6 +96,7 @@ var MenuItemView = Backbone.View.extend({
changeExpanded: function(model, expanded) {
this.$el.toggleClass('menu__item--collapsed', !expanded);
this.model.setExpanded(expanded);
},
changeCls: function(model, cls) {

View File

@ -4,11 +4,10 @@ var Backbone = require('backbone'),
Keys = require('../const/keys'),
Alerts = require('../comp/alerts'),
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');
DropboxLink = require('../comp/dropbox-link'),
Logger = require('../util/logger');
var logger = new Logger('open-view');
var OpenView = Backbone.View.extend({
template: require('templates/open.html'),
@ -31,25 +30,24 @@ var OpenView = Backbone.View.extend({
'drop': 'drop'
},
fileData: null,
keyFileData: null,
params: null,
passwordInput: null,
dropboxLoading: null,
busy: false,
initialize: function () {
this.setFileModel(new FileModel());
this.fileData = null;
this.keyFileData = null;
this.params = {
id: null,
name: '',
storage: null,
path: null,
keyFileName: null,
keyFileData: null,
fileData: null,
rev: null
};
this.passwordInput = new SecureInput();
},
setFileModel: function(file) {
this.file = file;
this.listenTo(this.file, 'change:open', this.fileOpenChanged);
this.listenTo(this.file, 'change:opening', this.fileOpeningChanged);
this.listenTo(this.file, 'change:error', this.fileErrorChanged);
},
render: function () {
if (this.dragTimeout) {
clearTimeout(this.dragTimeout);
@ -61,16 +59,24 @@ var OpenView = Backbone.View.extend({
},
getLastOpenFiles: function() {
return LastOpenFiles.all().map(function(f) {
switch (f.storage) {
return this.model.fileInfos.map(function(f) {
var icon;
switch (f.get('storage')) {
case 'dropbox':
f.icon = 'dropbox';
icon = 'dropbox';
break;
case 'file':
icon = 'hdd-o';
break;
default:
f.icon = 'file-text';
icon = 'file-text';
break;
}
return f;
return {
id: f.get('id'),
name: f.get('name'),
icon: icon
};
});
},
@ -79,30 +85,6 @@ var OpenView = Backbone.View.extend({
Backbone.View.prototype.remove.apply(this, arguments);
},
fileOpenChanged: function() {
this.model.addFile(this.file);
},
fileOpeningChanged: function() {
var opening = this.file.get('opening');
this.$el.toggleClass('open--opening', opening);
if (opening) {
this.inputEl.attr('disabled', 'disabled');
this.$el.find('#open__settings-check-offline').attr('disabled', 'disabled');
} else {
this.inputEl.removeAttr('disabled');
this.$el.find('#open__settings-check-offline').removeAttr('disabled');
}
},
fileErrorChanged: function() {
if (this.file.get('error')) {
this.inputEl.addClass('input--error').focus();
this.inputEl[0].selectionStart = 0;
this.inputEl[0].selectionEnd = this.inputEl.val().length;
}
},
fileSelected: function(e) {
var file = e.target.files[0];
if (file) {
@ -113,15 +95,17 @@ var OpenView = Backbone.View.extend({
processFile: function(file, complete) {
var reader = new FileReader();
reader.onload = (function(e) {
this[this.reading] = e.target.result;
if (this.reading === 'fileData') {
this.file.set({ name: file.name.replace(/\.\w+$/i, ''), offline: false });
if (file.path) {
this.file.set({ path: file.path, storage: file.storage || 'file' });
}
this.params.id = null;
this.params.fileData = e.target.result;
this.params.name = file.name.replace(/\.\w+$/i, '');
this.params.path = file.path || null;
this.params.storage = file.path ? 'file' : null;
this.params.rev = null;
this.displayOpenFile();
} else {
this.file.set('keyFileName', file.name);
this.params.keyFileData = e.target.result;
this.params.keyFileName = file.name;
this.displayOpenKeyFile();
}
if (complete) {
@ -130,7 +114,6 @@ var OpenView = Backbone.View.extend({
}).bind(this);
reader.onerror = (function() {
Alerts.error({ header: 'Failed to read file' });
this.showReadyToOpen();
if (complete) {
complete(false);
}
@ -141,17 +124,13 @@ var OpenView = Backbone.View.extend({
displayOpenFile: function() {
this.$el.addClass('open--file');
this.$el.find('.open__settings-key-file').removeClass('hide');
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[0].setAttribute('placeholder', 'Password for ' + this.params.name);
this.inputEl.focus();
},
displayOpenKeyFile: function() {
this.$el.find('.open__settings-key-file-name').text(this.file.get('keyFileName'));
this.$el.find('.open__settings-key-file-name').text(this.params.keyFileName);
this.$el.addClass('open--key-file');
this.inputEl.focus();
},
@ -166,54 +145,8 @@ var OpenView = Backbone.View.extend({
}).bind(this));
},
createDemo: function() {
if (!this.file.get('opening')) {
if (!this.model.files.getByName('Demo')) {
this.file.createDemo();
} else {
this.trigger('cancel');
}
}
},
createNew: function() {
if (!this.file.get('opening')) {
var name;
for (var i = 0; ; i++) {
name = 'New' + (i || '');
if (!this.model.files.getByName(name)) {
break;
}
}
this.file.create(name);
}
},
showOpenLocalFile: function(path) {
if (path && Launcher) {
try {
var name = path.match(/[^/\\]*$/)[0];
var data = Launcher.readFile(path);
var file = new Blob([data]);
Object.defineProperties(file, {
path: { value: path },
name: { value: name }
});
this.setFile(file);
} catch (e) {
console.log('Failed to show local file', e);
}
}
},
showClosedFile: function(file) {
this.setFileModel(file);
this.fileData = file.data;
this.displayOpenFile();
},
openFile: function() {
if (!this.file.get('opening')) {
if (!this.busy) {
this.openAny('fileData');
}
},
@ -221,10 +154,10 @@ var OpenView = Backbone.View.extend({
openKeyFile: function(e) {
if ($(e.target).hasClass('open__settings-key-file-dropbox')) {
this.openKeyFileFromDropbox();
} else if (!this.file.get('opening') && this.file.get('name')) {
if (this.keyFileData) {
this.keyFileData = null;
this.file.set('keyFileName', '');
} else if (!this.busy && this.params.name) {
if (this.params.keyFileData) {
this.params.keyFileData = null;
this.params.keyFileName = '';
this.$el.removeClass('open--key-file');
this.$el.find('.open__settings-key-file-name').text('key file');
} else {
@ -234,13 +167,13 @@ var OpenView = Backbone.View.extend({
},
openKeyFileFromDropbox: function() {
if (!this.file.get('opening')) {
if (!this.busy) {
DropboxLink.chooseFile((function(err, res) {
if (err) {
return;
}
this.keyFileData = res.data;
this.file.set('keyFileName', res.name);
this.params.keyFileData = res.data;
this.params.keyFileName = res.name;
this.displayOpenKeyFile();
}).bind(this));
}
@ -248,32 +181,28 @@ var OpenView = Backbone.View.extend({
openAny: function(reading, ext) {
this.reading = reading;
this[reading] = null;
this.params[reading] = null;
this.$el.find('.open__file-ctrl').attr('accept', ext || '').val(null).click();
},
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,
keyFileData: this.keyFileData
};
this.file.set({opening: true, error: false});
this.afterPaint(function () {
this.file.open(arg.password, arg.fileData, arg.keyFileData);
});
openLast: function(e) {
if (this.busy) {
return;
}
var id = $(e.target).closest('.open__last-item').data('id').toString();
if ($(e.target).is('.open__last-item-icon-del')) {
this.model.removeFileInfo(id);
this.$el.find('.open__last-item[data-id="' + id + '"]').remove();
this.initialize();
this.render();
return;
}
this.showOpenFileInfo(this.model.fileInfos.get(id));
},
inputKeydown: function(e) {
var code = e.keyCode || e.which;
if (code === Keys.DOM_VK_RETURN && this.passwordInput.length) {
if (code === Keys.DOM_VK_RETURN) {
this.openDb();
} else if (code === Keys.DOM_VK_CAPS_LOCK) {
this.$el.find('.open__pass-warning').removeClass('invisible');
@ -296,145 +225,6 @@ 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, dirStat) {
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.' +
(dirStat && dirStat.inAppFolder ? ' Files are searched inside app folder in your Dropbox.' : '')
});
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').toString();
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);
case 'file':
return this.showOpenLocalFile(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) {
@ -466,6 +256,143 @@ var OpenView = Backbone.View.extend({
if (dataFile) {
this.setFile(dataFile, keyFile);
}
},
displayDropboxLoading: function(isLoading) {
this.$el.find('.open__icon-dropbox .open__icon-i').toggleClass('flip3d', !!isLoading);
},
openFromDropbox: function() {
if (this.busy) {
return;
}
var that = this;
DropboxLink.authenticate(function(err) {
if (err) {
return;
}
that.busy = true;
that.displayDropboxLoading(true);
DropboxLink.getFileList(function(err, files, dirStat, filesStat) {
that.busy = false;
that.displayDropboxLoading(false);
if (err) {
return;
}
var buttons = [];
var allDropboxFiles = {};
filesStat.forEach(function(file) {
if (!file.isFolder && !file.isRemoved) {
var fileName = file.name.replace(/\.kdbx/i, '');
buttons.push({ result: file.path, title: fileName });
allDropboxFiles[file.path] = file;
}
});
if (!buttons.length) {
Alerts.error({
header: 'Nothing found',
body: 'You have no files in your Dropbox which could be opened.' +
(dirStat && dirStat.inAppFolder ? ' Files are searched inside app folder in your Dropbox.' : '')
});
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: function(file) {
that.openDropboxFile(allDropboxFiles[file]);
}
});
that.model.fileInfos.forEach(function(fi) {
if (fi.get('storage') === 'dropbox' && !fi.get('modified') && !allDropboxFiles[fi.get('path')]) {
that.model.removeFileInfo(fi.id);
}
});
});
});
},
openDropboxFile: function(fileStat) {
if (this.busy) {
return;
}
this.params.id = null;
this.params.storage = 'dropbox';
this.params.path = fileStat.path;
this.params.name = fileStat.name.replace(/\.kdbx/i, '');
this.params.rev = fileStat.versionTag;
this.params.fileData = null;
this.displayOpenFile();
},
showOpenFileInfo: function(fileInfo) {
if (this.busy || !fileInfo) {
return;
}
this.params.id = fileInfo.id;
this.params.storage = fileInfo.get('storage');
this.params.path = fileInfo.get('path');
this.params.name = fileInfo.get('name');
this.params.fileData = null;
this.params.rev = null;
this.displayOpenFile();
},
showOpenLocalFile: function(path) {
if (this.busy) {
return;
}
this.params.id = null;
this.params.storage = 'file';
this.params.path = path;
this.params.name = path.match(/[^/\\]*$/)[0];
this.params.rev = null;
this.params.fileData = null;
this.displayOpenFile();
},
createDemo: function() {
if (!this.busy) {
if (!this.model.createDemoFile()) {
this.trigger('close');
}
}
},
createNew: function() {
if (!this.busy) {
this.model.createNewFile();
}
},
openDb: function() {
if (this.busy || !this.params.name) {
return;
}
this.$el.toggleClass('open--opening', true);
this.inputEl.attr('disabled', 'disabled');
this.busy = true;
this.params.password = this.passwordInput.value;
this.afterPaint(this.model.openFile.bind(this.model, this.params, this.openDbComplete.bind(this)));
},
openDbComplete: function(err) {
this.busy = false;
this.$el.toggleClass('open--opening', false);
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err);
if (err) {
logger.error('Error opening file', err);
this.inputEl.focus();
this.inputEl[0].selectionStart = 0;
this.inputEl[0].selectionEnd = this.inputEl.val().length;
} else {
this.trigger('close');
}
}
});

View File

@ -5,8 +5,10 @@ var Backbone = require('backbone'),
PasswordGenerator = require('../../util/password-generator'),
Alerts = require('../../comp/alerts'),
Launcher = require('../../comp/launcher'),
Storage = require('../../storage'),
Links = require('../../const/links'),
DropboxLink = require('../../comp/dropbox-link'),
Format = require('../../util/format'),
kdbxweb = require('kdbxweb'),
FileSaver = require('filesaver');
@ -14,9 +16,10 @@ var SettingsAboutView = Backbone.View.extend({
template: require('templates/settings/settings-file.html'),
events: {
'click .settings__file-button-save-default': 'saveDefault',
'click .settings__file-button-save-file': 'saveToFile',
'click .settings__file-button-export-xml': 'exportAsXml',
'click .settings__file-button-save-dropbox': 'saveToDropboxClick',
'click .settings__file-button-save-dropbox': 'saveToDropbox',
'click .settings__file-button-close': 'closeFile',
'change #settings__file-key-file': 'keyFileChange',
'mousedown #settings__file-file-select-link': 'triggerSelectFile',
@ -31,7 +34,10 @@ var SettingsAboutView = Backbone.View.extend({
'blur #settings__file-key-rounds': 'blurKeyRounds'
},
appModel: null,
initialize: function() {
this.listenTo(this.model, 'change:syncing change:syncError change:syncDate', this.render);
},
render: function() {
@ -44,6 +50,8 @@ var SettingsAboutView = Backbone.View.extend({
path: this.model.get('path'),
storage: this.model.get('storage'),
syncing: this.model.get('syncing'),
syncError: this.model.get('syncError'),
syncDate: Format.dtStr(this.model.get('syncDate')),
password: PasswordGenerator.present(this.model.get('passwordLength')),
defaultUser: this.model.get('defaultUser'),
recycleBinEnabled: this.model.get('recycleBinEnabled'),
@ -81,122 +89,125 @@ var SettingsAboutView = Backbone.View.extend({
}
},
validate: function() {
validatePassword: function(continueCallback) {
if (!this.model.get('passwordLength')) {
Alerts.error({
var that = this;
Alerts.yesno({
header: 'Empty password',
body: 'Please, enter the password. You will use it the next time you open this file.',
complete: (function() { this.$el.find('#settings__file-master-pass').focus(); }).bind(this)
body: 'Saving database with empty password makes it completely unprotected. Do you really want to do it?',
success: function() {
continueCallback();
},
cancel: function() {
that.$el.find('#settings__file-master-pass').focus();
}
});
return false;
}
return true;
},
saveToFile: function() {
if (!this.validate()) {
return;
}
save: function(arg) {
var that = this;
this.model.getData(function(data) {
var fileName = that.model.get('name') + '.kdbx';
if (Launcher) {
if (that.model.get('path')) {
that.saveToFileWithPath(that.model.get('path'), data);
} else {
Launcher.getSaveFileName(fileName, function (path) {
if (path) {
that.saveToFileWithPath(path, data);
}
});
}
} else {
var blob = new Blob([data], {type: 'application/octet-stream'});
FileSaver.saveAs(blob, fileName);
that.passwordChanged = false;
if (that.model.get('storage') !== 'dropbox') {
that.model.saved();
}
if (!arg) {
arg = {};
}
arg.startedByUser = true;
if (!arg.skipValidation) {
var isValid = this.validatePassword(function() {
arg.skipValidation = true;
that.save(arg);
});
if (!isValid) {
return;
}
});
}
this.appModel.syncFile(this.model, arg);
},
saveToFileWithPath: function(path, data) {
try {
Launcher.writeFile(path, data);
this.passwordChanged = false;
this.model.saved(path, 'file');
} catch (e) {
console.error('Error saving file', path, e);
Alerts.error({
header: 'Save error',
body: 'Error saving to file ' + path + ': \n' + e
saveDefault: function() {
this.save();
},
saveToFile: function(skipValidation) {
if (skipValidation !== true && !this.validatePassword(this.saveToFile.bind(this, true))) {
return;
}
var fileName = this.model.get('name') + '.kdbx';
var that = this;
if (Launcher && !this.model.get('storage')) {
Launcher.getSaveFileName(fileName, function (path) {
if (path) {
that.save({storage: 'file', path: path});
}
});
} else {
this.model.getData(function (data) {
if (Launcher) {
Launcher.getSaveFileName(fileName, function (path) {
if (path) {
Storage.file.save(path, data, function (err) {
if (err) {
Alerts.error({
header: 'Save error',
body: 'Error saving to file ' + path + ': \n' + err
});
}
});
}
});
} else {
var blob = new Blob([data], {type: 'application/octet-stream'});
FileSaver.saveAs(blob, fileName);
}
});
}
},
exportAsXml: function() {
if (!this.validate()) {
return;
}
this.model.getXml((function(xml) {
var blob = new Blob([xml], {type: 'text/xml'});
FileSaver.saveAs(blob, this.model.get('name') + '.xml');
}).bind(this));
},
saveToDropboxClick: function() {
var nameChanged = this.model.get('path') !== this.model.get('name') + '.kdbx',
canOverwrite = !nameChanged;
this.saveToDropbox(canOverwrite);
},
saveToDropbox: function(overwrite) {
if (this.model.get('syncing') || !this.validate()) {
return;
}
saveToDropbox: function() {
var that = this;
this.model.getData(function(data) {
var fileName = that.model.get('name') + '.kdbx';
that.model.set('syncing', true);
that.render();
DropboxLink.authenticate(function(err) {
if (err) {
this.model.set('syncing', true);
DropboxLink.authenticate(function(err) {
that.model.set('syncing', false);
if (err) {
return;
}
if (that.model.get('storage') === 'dropbox') {
that.save();
} else {
that.model.set('syncing', true);
DropboxLink.getFileList(function(err, files) {
that.model.set('syncing', false);
that.render();
return;
}
DropboxLink.saveFile(fileName, data, overwrite, function (err) {
if (err) {
that.model.set('syncing', false);
that.render();
if (err.exists) {
Alerts.alert({
header: 'Already exists',
body: 'File ' + fileName + ' already exists in your Dropbox.',
icon: 'question',
buttons: [{result: 'yes', title: 'Overwrite it'}, {result: '', title: 'I\'ll choose another name'}],
esc: '',
click: '',
enter: 'yes',
success: that.saveToDropbox.bind(that, true),
cancel: function () {
that.$el.find('#settings__file-name').focus();
}
});
} else {
Alerts.error({
header: 'Save error',
body: 'Error saving to Dropbox: \n' + err
});
}
if (!files) { return; }
var expName = that.model.get('name').toLowerCase();
var existingPath = files.filter(function(f) { return f.toLowerCase().replace('/', '') === expName; })[0];
if (existingPath) {
Alerts.yesno({
icon: 'dropbox',
header: 'Already exists',
body: 'File ' + that.model.escape('name') + ' already exists in your Dropbox. Overwrite it?',
success: function() {
that.model.set('syncing', true);
DropboxLink.deleteFile(existingPath, function(err) {
that.model.set('syncing', false);
if (!err) {
that.save({storage: 'dropbox'});
}
});
}
});
} else {
that.passwordChanged = false;
that.model.saved(fileName, 'dropbox');
that.render();
that.save({storage: 'dropbox'});
}
});
});
}
});
},
@ -223,7 +234,7 @@ var SettingsAboutView = Backbone.View.extend({
},
closeFileNoCheck: function() {
Backbone.trigger('close-file', this.model);
this.appModel.closeFile(this.model);
},
keyFileChange: function(e) {
@ -273,20 +284,16 @@ var SettingsAboutView = Backbone.View.extend({
},
focusMasterPass: function(e) {
if (!this.passwordChanged) {
e.target.value = '';
}
e.target.value = '';
e.target.setAttribute('type', 'text');
},
blurMasterPass: function(e) {
if (!e.target.value) {
this.passwordChanged = false;
this.model.resetPassword();
e.target.value = PasswordGenerator.present(this.model.get('passwordLength'));
this.$el.find('.settings__file-master-pass-warning').hide();
} else {
this.passwordChanged = true;
this.model.setPassword(kdbxweb.ProtectedValue.fromString(e.target.value));
if (!this.model.get('created')) {
this.$el.find('.settings__file-master-pass-warning').show();

View File

@ -21,7 +21,9 @@ var SettingsGeneralView = Backbone.View.extend({
'change .settings__general-clipboard': 'changeClipboard',
'change .settings__general-auto-save': 'changeAutoSave',
'change .settings__general-minimize': 'changeMinimize',
'change .settings__general-lock-on-minimize': 'changeLockOnMinimize',
'change .settings__general-table-view': 'changeTableView',
'change .settings__general-colorful-icons': 'changeColorfulIcons',
'click .settings__general-update-btn': 'checkUpdate',
'click .settings__general-restart-btn': 'restartApp',
'click .settings__general-download-update-btn': 'downloadUpdate',
@ -30,9 +32,9 @@ var SettingsGeneralView = Backbone.View.extend({
},
allThemes: {
d: 'default',
fb: 'flat blue',
wh: 'white'
fb: 'Flat blue',
db: 'Dark brown',
wh: 'White'
},
initialize: function() {
@ -52,7 +54,8 @@ var SettingsGeneralView = Backbone.View.extend({
minimizeOnClose: AppSettingsModel.instance.get('minimizeOnClose'),
devTools: Launcher && Launcher.devTools,
canAutoUpdate: !!Launcher,
canMinimizeOnClose: Launcher && Launcher.canMinimize(),
canMinimize: Launcher && Launcher.canMinimize(),
lockOnMinimize: Launcher && AppSettingsModel.instance.get('lockOnMinimize'),
tableView: AppSettingsModel.instance.get('tableView'),
canSetTableView: FeatureDetector.isDesktop(),
autoUpdate: Updater.getAutoUpdateType(),
@ -61,7 +64,8 @@ var SettingsGeneralView = Backbone.View.extend({
updateReady: UpdateModel.instance.get('updateStatus') === 'ready',
updateFound: UpdateModel.instance.get('updateStatus') === 'found',
updateManual: UpdateModel.instance.get('updateManual'),
releaseNotesLink: Links.ReleaseNotes
releaseNotesLink: Links.ReleaseNotes,
colorfulIcons: AppSettingsModel.instance.get('colorfulIcons')
});
},
@ -138,12 +142,23 @@ var SettingsGeneralView = Backbone.View.extend({
AppSettingsModel.instance.set('minimizeOnClose', minimizeOnClose);
},
changeLockOnMinimize: function(e) {
var lockOnMinimize = e.target.checked || false;
AppSettingsModel.instance.set('lockOnMinimize', lockOnMinimize);
},
changeTableView: function(e) {
var tableView = e.target.checked || false;
AppSettingsModel.instance.set('tableView', tableView);
Backbone.trigger('refresh');
},
changeColorfulIcons: function(e) {
var colorfulIcons = e.target.checked || false;
AppSettingsModel.instance.set('colorfulIcons', colorfulIcons);
Backbone.trigger('refresh');
},
restartApp: function() {
if (Launcher) {
Launcher.requestRestart();

View File

@ -46,6 +46,7 @@ var SettingsView = Backbone.View.extend({
}
var SettingsPageView = require('./settings-' + e.page + '-view');
this.views.page = new SettingsPageView({ el: this.pageEl, model: e.file });
this.views.page.appModel = this.model;
this.views.page.render();
this.file = e.file;
this.page = e.page;

View File

@ -130,6 +130,10 @@
@include flex-wrap(wrap);
overflow-x: hidden;
padding-top: 3px;
@include scrollbar-full-width-hack();
@-moz-document url-prefix() { @include scrollbar-padding-hack(); }
@at-root { _:-ms-lang(x), .details__body>.scroller { @include scrollbar-padding-hack(); } }
}
&-fields {

View File

@ -32,6 +32,7 @@
right: 1em;
top: 1em;
@include th { color: action-color(); }
&--error { @include th { color: error-color(); } }
}
}

View File

@ -21,6 +21,11 @@
@include flex(1);
@include align-self(stretch);
position: relative;
.app__list-wrap--table & {
@include scrollbar-full-width-hack();
@-moz-document url-prefix() { @include scrollbar-padding-hack(); }
@at-root { _:-ms-lang(x), .app__list-wrap--table .list__items>.scroller { @include scrollbar-padding-hack(); } }
}
@include mobile { width: 100% !important; }
}
}
@ -74,7 +79,7 @@
}
&__table {
width: calc(100% - 1px);
width: calc(100% - 2px);
td, th {
padding: $base-padding;
text-align: left;
@ -104,7 +109,6 @@
height: 14px;
&--custom {
vertical-align: text-bottom;
@include filter(grayscale(1));
&.yellow { @include filter(grayscale(1) sepia(1) hue-rotate(20deg) brightness(1.17) saturate(5.7)); }
&.green { @include filter(grayscale(1) sepia(1) hue-rotate(55deg) brightness(1.01) saturate(4.9)); }
&.red { @include filter(grayscale(1) sepia(1) hue-rotate(316deg) brightness(1.1) saturate(6)); }

View File

@ -103,8 +103,7 @@
}
}
&-key-file, &-label-offline, &-label-offline:before, input[type=checkbox] + label.open__settings-label-offline:before,
&-key-file-dropbox {
&-key-file, &-key-file-dropbox {
@include th {
color: muted-color();
}
@ -114,11 +113,6 @@
}
}
}
&-label-offline { font-weight: normal; }
}
&--file:not(.open--opening) input[type=checkbox] + label.open__settings-label-offline:hover:before {
@include th { color: medium-color(); }
}
&__last {

View File

@ -59,3 +59,4 @@ $all-colors: (
}
.muted-color { @include th { color: muted-color(); }; }
.action-color { @include th { color: action-color(); }; }
.error-color { @include th { color: error-color(); }; }

View File

@ -155,7 +155,7 @@ option {
input[type=checkbox] {
display: none;
& + label:hover:before {
&:not([disabled]) + label:hover:before {
@include th {
color: action-color();
}

View File

@ -1,12 +1,23 @@
@mixin scrollbar-padding-hack {
// workaround for bugs in custom scrollbar component (baron)
// https://github.com/Diokuz/baron/issues/91
// https://github.com/Diokuz/baron/issues/93
margin-right: -30px !important;
padding-right: 30px !important;
}
@mixin scrollbar-full-width-hack {
// workaround for bugs in custom scrollbar component (baron)
width: auto !important;
min-width: 0 !important;
max-width: none !important;
}
.scroller {
overflow-y: scroll;
height: 100%;
@-moz-document url-prefix() {
// until Firefox bug is fixed in baron, hide native scroll area manually
// https://github.com/Diokuz/baron/issues/91
// https://github.com/Diokuz/baron/issues/93
margin-right: -30px;
padding-right: 30px;
@include scrollbar-padding-hack();
}
&::-webkit-scrollbar {
width: 0;
@ -31,6 +42,15 @@
}
}
@media screen and (-moz-windows-theme) {
.scroller {
@-moz-document url-prefix() {
margin-right: 0 !important;
padding-right: 0 !important;
}
}
}
@mixin scrollbar-on-hover {
.scroller__bar-wrapper {
>.scroller__bar {

View File

@ -1,5 +1,5 @@
$themes: ();
@import "default";
@import "dark-brown";
@import "flat-blue";
@import "white";

View File

@ -1,5 +1,5 @@
$themes: map-merge($themes, (
d: (
db: (
background-color: #342F2E,
medium-color: #FED9D8,
text-color: #FFEAE9,

View File

@ -2,8 +2,10 @@
<% files.forEach(function(file) { %>
<div class="footer__db footer__db-item <%= file.get('open') ? '' : 'footer__db--dimmed' %>" data-file-id="<%= file.cid %>">
<i class="fa fa-<%= file.get('open') ? 'unlock' : 'lock' %>"></i> <%- file.get('name') %>
<% if (file.get('modified') && !file.get('syncing')) { %><i class="fa fa-circle footer__db-sign"></i><% } %>
<% if (file.get('syncing')) { %><i class="fa fa-refresh fa-spin footer__db-sign"></i><% } %>
<% if (file.get('syncing')) { %><i class="fa fa-refresh fa-spin footer__db-sign"></i><% }
else if (file.get('syncError')) { %><i class="fa <%= file.get('modified') ? 'fa-circle' : 'fa-circle-thin' %> footer__db-sign footer__db-sign--error"
title="Sync error: <%- file.get('syncError') %>"></i><% }
else if (file.get('modified')) { %><i class="fa fa-circle footer__db-sign"></i><% } %>
</div>
<% }); %>
<div class="footer__db footer__db--dimmed footer__db--expanded footer__db-open"><i class="fa fa-plus"></i> Open / New</div>

View File

@ -9,6 +9,12 @@
<input type="text" class="input-base" id="grp__field-title" value="<%- title %>" size="50" maxlength="1024"
required <%= readonly ? 'readonly' : '' %> />
</div>
<% if (!readonly) { %>
<div>
<input type="checkbox" class="input-base" id="grp__check-search" <%= enableSearching ? 'checked' : '' %> />
<label for="grp__check-search">Enable searching entries in this group</label>
</div>
<% } %>
<label>Icon:</label>
<% if (customIcon) { %>
<img src="<%= customIcon %>" class="grp__icon grp__icon--image" />

View File

@ -2,7 +2,7 @@
<td><% if (customIcon) { %><img src="<%= customIcon %>" class="list__item-icon list__item-icon--custom <%= color || '' %>" /><% }
else { %><i class="fa fa-<%= icon %> <%= color ? color+'-color' : '' %> list__item-icon"></i><% } %></td>
<td><%- title || '(no title)' %></td>
<td><%- description %></td>
<td><%- user %></td>
<td><%- url %></td>
<td><%- tags %></td>
<td><%- notes %></td>

View File

@ -34,15 +34,10 @@
<span class="open__settings-key-file-name">key file</span>
<span class="open__settings-key-file-dropbox"> (from dropbox)</span>
</div>
<div class="open__settings-offline hide">
<input type="checkbox" id="open__settings-check-offline" class="open__settings-check-offline" checked disabled />
<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">
<% lastOpenFiles.forEach(function(file) { %>
<div class="open__last-item" data-name="<%- file.name %>">
<div class="open__last-item" data-id="<%- file.id %>">
<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>

View File

@ -7,21 +7,27 @@
<p>This file is opened from Dropbox.</p>
<% } %>
<% } else { %>
<p>This database is loaded in memory. To enable auto-save and saving with shortcut <%= cmd %>S,
please, save it to <%= supportFiles ? 'file or ' : '' %>Dropbox.</p>
<p>This file is stored in internal app storage.</p>
<% if (!supportFiles) { %>
<p>Want to work seamlessly with local files? <a href="<%= desktopLink %>" target="_blank">Download a desktop app</a></p>
<% } %>
<% } %>
<div class="settings__file-buttons">
<button class="settings__file-button-save-file btn-silent">Save to file</button>
<% if (!storage || storage === 'file') { %><button class="settings__file-button-save-default">Save</button><% } %>
<button class="settings__file-button-save-dropbox <%= storage !== 'dropbox' ? 'btn-silent' : '' %>"
<%= syncing ? 'disabled' : '' %>>Sync with Dropbox</button>
<% if (storage !== 'file') { %><button class="settings__file-button-save-file btn-silent">Save to file</button><% } %>
<button class="settings__file-button-export-xml btn-silent">Export to XML</button>
<button class="settings__file-button-save-dropbox btn-silent" <%= syncing ? 'disabled' : '' %>>
Sync with Dropbox <%= syncing ? '(working...)' : '' %></button>
<button class="settings__file-button-close btn-silent">Close</button>
</div>
<% if (storage) { %>
<h2>Sync</h2>
<div>Last sync: <%= syncDate || 'unknown' %> <%= syncing ? '(sync in progress...)' : '' %></div>
<% if (syncError) { %><div>Sync error: <%- syncError %></div><% } %>
<% } %>
<h2>Settings</h2>
<label for="settings__file-master-pass" class="settings__file-master-pass-label input-base">Master password:
<span class="settings__file-master-pass-warning">

View File

@ -56,6 +56,10 @@
<label for="settings__general-table-view">Entries list table view</label>
</div>
<% } %>
<div>
<input type="checkbox" class="settings__input input-base settings__general-colorful-icons" id="settings__general-colorful-icons" <%- colorfulIcons ? 'checked' : '' %> />
<label for="settings__general-colorful-icons">Colorful custom icons in list</label>
</div>
<h2>Function</h2>
<div>
@ -85,13 +89,18 @@
</select>
</div>
<% } %>
<% if (canMinimizeOnClose) { %>
<% if (canMinimize) { %>
<div>
<input type="checkbox" class="settings__input input-base settings__general-minimize" id="settings__general-minimize"
<%- minimizeOnClose ? 'checked' : '' %> />
<%- minimizeOnClose ? 'checked' : '' %> />
<label for="settings__general-minimize">Minimize app instead of close</label>
</div>
<% } %>
<div>
<input type="checkbox" class="settings__input input-base settings__general-lock-on-minimize" id="settings__general-lock-on-minimize"
<%- lockOnMinimize ? 'checked' : '' %> />
<label for="settings__general-lock-on-minimize">Auto-lock on minimize</label>
</div>
<% if (devTools) { %>
<h2>Advanced</h2>

View File

@ -29,5 +29,5 @@
</li>
</ul>
<h2>Updates <i class="fa fa-twitter"></i></h2>
<p>App twitter: <a href="https://twitter.com/kee_web">kee_web</a></p>
<p>App twitter: <a href="https://twitter.com/kee_web" target="_blank">kee_web</a></p>
</div>

View File

@ -24,12 +24,12 @@
"private": true,
"dependencies": {
"backbone": "~1.2.3",
"baron": "~0.7.11",
"baron": "~1.0.1",
"bourbon": "~4.2.5",
"dropbox": "antelle/dropbox-js#0.10.5",
"dropbox": "antelle/dropbox-js#0.10.6",
"font-awesome": "~4.4.0",
"install": "~1.0.4",
"kdbxweb": "~0.2.7",
"kdbxweb": "~0.3.2",
"normalize.css": "~3.0.3",
"pikaday": "~1.3.3",
"zepto": "~1.1.6",

View File

@ -28,15 +28,56 @@ app.on('window-all-closed', function() {
app.removeAllListeners('window-all-closed');
app.removeAllListeners('ready');
app.removeAllListeners('open-file');
app.removeAllListeners('activate');
var userDataAppFile = path.join(app.getPath('userData'), 'app.js');
delete require.cache[require.resolve('./app.js')];
require(userDataAppFile);
app.emit('ready');
} else {
app.quit();
if (process.platform !== 'darwin') {
app.quit();
}
}
});
app.on('ready', function() {
createMainWindow();
});
app.on('open-file', function(e, path) {
e.preventDefault();
openFile = path;
notifyOpenFile();
});
app.on('activate', function() {
if (process.platform === 'darwin') {
if (!mainWindow) {
createMainWindow();
}
}
});
app.restartApp = function() {
restartPending = true;
mainWindow.close();
setTimeout(function() { restartPending = false; }, 1000);
};
app.openWindow = function(opts) {
return new BrowserWindow(opts);
};
app.minimizeApp = function() {
if (process.platform === 'win32') {
mainWindow.minimize();
mainWindow.setSkipTaskbar(true);
appIcon = new Tray(path.join(__dirname, 'icon.png'));
appIcon.on('click', restoreMainWindow);
var contextMenu = Menu.buildFromTemplate([
{ label: 'Open KeeWeb', click: restoreMainWindow },
{ label: 'Quit KeeWeb', click: closeMainWindow }
]);
appIcon.setContextMenu(contextMenu);
appIcon.setToolTip('KeeWeb');
}
};
function createMainWindow() {
mainWindow = new BrowserWindow({
show: false,
width: 1000, height: 700, 'min-width': 600, 'min-height': 300,
@ -54,34 +95,10 @@ app.on('ready', function() {
mainWindow.on('closed', function() {
mainWindow = null;
});
});
app.on('open-file', function(e, path) {
e.preventDefault();
openFile = path;
notifyOpenFile();
});
app.restartApp = function() {
restartPending = true;
mainWindow.close();
setTimeout(function() { restartPending = false; }, 1000);
};
app.openWindow = function(opts) {
return new BrowserWindow(opts);
};
app.minimizeApp = function() {
if (process.platform === 'win32') {
mainWindow.minimize();
mainWindow.setSkipTaskbar(true);
appIcon = new Tray(path.join(__dirname, 'icon.png'));
appIcon.on('clicked', restoreMainWindow);
var contextMenu = Menu.buildFromTemplate([
{ label: 'Open KeeWeb', click: restoreMainWindow },
{ label: 'Quit KeeWeb', click: closeMainWindow }
]);
appIcon.setContextMenu(contextMenu);
appIcon.setToolTip('KeeWeb');
}
};
mainWindow.on('minimize', function() {
emitBackboneEvent('launcher-minimize');
});
}
function restoreMainWindow() {
appIcon.destroy();
@ -93,7 +110,11 @@ function restoreMainWindow() {
function closeMainWindow() {
appIcon.destroy();
appIcon = null;
mainWindow.webContents.executeJavaScript('Backbone.trigger("launcher-exit-request");');
emitBackboneEvent('launcher-exit-request');
}
function emitBackboneEvent(e) {
mainWindow.webContents.executeJavaScript('Backbone.trigger("' + e + '");');
}
function setMenu() {

View File

@ -10,6 +10,5 @@
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="keeweb node_modules" level="project" />
</component>
</module>

View File

@ -1,5 +1,17 @@
Release notes
-------------
##### v0.5 (not released yet)
2-way merge sync
`*` all files are now opened with offline support
`*` disallow opening same files twice
`*` default theme is now blue
`+` #46: option to show colorful icons
`+` #45: optional auto-lock on minimize
`+` option to disable searching for group
`+` #62: saving files with empty password
`+` #56: preserve selected entry after close
`-` #55: custom scrollbar issues
##### v0.4.6 (2015-11-25)
`-` #32: visual glitches on Windows 10