mirror of https://github.com/keeweb/keeweb.git
Merge branch 'develop'
This commit is contained in:
commit
7c89717991
|
@ -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>
|
|
@ -88,8 +88,6 @@
|
|||
"globals" : {
|
||||
"require": true,
|
||||
"module": true,
|
||||
"console": true,
|
||||
"performance": true,
|
||||
"$": true,
|
||||
"_": true
|
||||
}
|
||||
|
|
10
Gruntfile.js
10
Gruntfile.js
|
@ -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'
|
||||
]);
|
||||
|
||||
|
|
20
README.md
20
README.md
|
@ -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:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html manifest="manifest.appcache">
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>KeeWeb</title>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
CACHE MANIFEST
|
||||
|
||||
# YYYY-MM-DD:v0.0.0 evElectron
|
||||
# YYYY-MM-DD:v0.0.0
|
||||
|
||||
CACHE:
|
||||
index.html
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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; });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Timeouts = {
|
||||
AutoSync: 30 * 1000 * 60
|
||||
};
|
||||
|
||||
module.exports = Timeouts;
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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; },
|
||||
|
|
|
@ -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;
|
|
@ -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));
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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; } } }));
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
right: 1em;
|
||||
top: 1em;
|
||||
@include th { color: action-color(); }
|
||||
&--error { @include th { color: error-color(); } }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)); }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(); }; }
|
||||
|
|
|
@ -155,7 +155,7 @@ option {
|
|||
input[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
& + label:hover:before {
|
||||
&:not([disabled]) + label:hover:before {
|
||||
@include th {
|
||||
color: action-color();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
$themes: ();
|
||||
|
||||
@import "default";
|
||||
@import "dark-brown";
|
||||
@import "flat-blue";
|
||||
@import "white";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
$themes: map-merge($themes, (
|
||||
d: (
|
||||
db: (
|
||||
background-color: #342F2E,
|
||||
medium-color: #FED9D8,
|
||||
text-color: #FFEAE9,
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue