Merge branch 'master' into no-auto-update

Conflicts:
	app/scripts/app.js
	app/scripts/comp/launcher.js
	app/scripts/models/app-settings-model.js
	app/scripts/views/settings/settings-general-view.js
	app/templates/settings/settings-general.html
This commit is contained in:
Antelle 2015-11-11 22:26:04 +03:00
commit c7e7fe04ad
90 changed files with 2322 additions and 746 deletions

View File

@ -19,5 +19,8 @@ trim_trailing_whitespace = false
[*.json]
indent_size = 2
[.jshintrc]
indent_size = 2
[*.scss]
indent_size = 2

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
workspace.xml
misc.xml
.DS_Store
node_modules/
bower_components/

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="true" assert-keyword="true" jdk-15="true">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

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

View File

@ -8,10 +8,12 @@ var StringReplacePlugin = require('string-replace-webpack-plugin');
module.exports = function(grunt) {
require('time-grunt')(grunt);
require('load-grunt-tasks')(grunt);
grunt.loadTasks('grunt/tasks');
var webpack = require('webpack');
var pkg = require('./package.json');
var dt = new Date().toISOString().replace(/T.*/, '');
var electronVersion = '0.34.0';
function replaceFont(css) {
css.walkAtRules('font-face', function (rule) {
@ -37,12 +39,24 @@ module.exports = function(grunt) {
}
grunt.initConfig({
gitinfo: {
branch: {
current: {
SHA: 'Current HEAD SHA',
shortSHA: 'Current HEAD short SHA',
name: 'Current branch name',
lastCommitTime: 'Last commit time'
}
}
},
'bower-install-simple': {
install: {
}
},
clean: {
dist: ['dist', 'tmp']
dist: ['dist', 'tmp'],
desktop_dist: ['dist/desktop'],
desktop_tmp: ['tmp/desktop']
},
copy: {
html: {
@ -61,6 +75,21 @@ module.exports = function(grunt) {
nonull: true,
expand: true,
flatten: true
},
'desktop_osx': {
src: 'tmp/desktop/KeeWeb.dmg',
dest: 'dist/desktop/KeeWeb.mac.dmg',
nonull: true
},
'desktop_win': {
src: 'tmp/desktop/KeeWeb Setup.exe',
dest: 'dist/desktop/KeeWeb.win32.exe',
nonull: true
},
'desktop_linux': {
src: 'tmp/desktop/KeeWeb.linux.x64.zip',
dest: 'dist/desktop/KeeWeb.linux.x64.zip',
nonull: true
}
},
jshint: {
@ -114,7 +143,7 @@ module.exports = function(grunt) {
options: {
replacements: [{
pattern: '# YYYY-MM-DD:v0.0.0',
replacement: '# ' + dt + ':v' + require('./package').version
replacement: '# ' + dt + ':v' + pkg.version
}]
},
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
@ -218,6 +247,90 @@ module.exports = function(grunt) {
files: 'app/index.html',
tasks: ['copy:html']
}
},
electron: {
options: {
name: 'KeeWeb',
dir: 'electron',
out: 'tmp/desktop',
version: electronVersion,
overwrite: true,
'app-version': pkg.version,
'build-version': '<%= gitinfo.local.branch.current.shortSHA %>'
},
osx: {
options: {
platform: 'darwin',
arch: 'x64',
icon: 'graphics/app.icns'
}
},
linux: {
options: {
platform: 'linux',
arch: 'x64',
icon: 'graphics/app.ico'
}
},
win32: {
options: {
platform: 'win32',
arch: 'ia32',
icon: 'graphics/app.ico',
'version-string': {
CompanyName: 'antelle.github.io',
LegalCopyright: 'Antelle, MIT license',
FileDescription: 'KeeWeb Desktop',
OriginalFilename: 'KeeWeb.exe',
FileVersion: pkg.version,
ProductVersion: pkg.version,
ProductName: 'KeeWeb',
InternalName: 'KeeWeb'
}
}
}
},
'electron_builder': {
options: {
out: path.join(__dirname, 'tmp/desktop'),
basePath: __dirname,
config: {
osx: {
title: 'KeeWeb',
background: path.join(__dirname, 'graphics/dmg-bg.png'),
icon: path.join(__dirname, 'graphics/app.icns'),
'icon-size': 80,
contents: [
{'x': 438, 'y': 344, 'type': 'link', 'path': '/Applications'},
{'x': 192, 'y': 344, 'type': 'file'}
]
},
win: {
title: 'KeeWeb',
icon: path.join(__dirname, 'graphics/app.ico')
}
}
},
osx: {
options: {
platform: 'osx',
appPath: path.join(__dirname, 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app')
}
},
win: {
options: {
platform: 'win32',
appPath: path.join(__dirname, 'tmp/desktop/KeeWeb-win32-ia32')
}
}
},
compress: {
linux: {
options: {
archive: 'tmp/desktop/KeeWeb.linux.x64.zip'
},
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true }]
}
}
});
@ -236,4 +349,17 @@ module.exports = function(grunt) {
'htmlmin',
'string-replace'
]);
grunt.registerTask('desktop', [
'gitinfo',
'clean:desktop_tmp',
'clean:desktop_dist',
'electron',
'electron_builder',
'compress:linux',
'copy:desktop_osx',
'copy:desktop_win',
'copy:desktop_linux',
'clean:desktop_tmp'
]);
};

View File

@ -3,30 +3,46 @@
This webapp can read KeePass databases. It doesn't require any server or additional resources.
It can be run either in browser, or as a desktop app.
![screenshot](https://antelle.github.io/keeweb/screenshot2x.png)
![screenshot](https://habrastorage.org/files/bfb/51e/d8d/bfb51ed8d19847d8afb827c4fbff7dd5.png)
# Quick Links
[Web](https://antelle.github.io/keeweb/)
[Windows](https://github.com/antelle/keeweb/releases/download/v0.0.1/KeeWeb.win32.exe)
[Mac OSX](https://github.com/antelle/keeweb/releases/download/v0.0.1/KeeWeb.mac.dmg)
[Linux](https://github.com/antelle/keeweb/releases/download/v0.0.1/KeeWeb.linux.x64.zip)
Web: [Web App](https://antelle.github.io/keeweb/)
Desktop: [Windows](https://github.com/antelle/keeweb/releases/download/v0.2.0/KeeWeb.win32.exe)
[Mac OSX](https://github.com/antelle/keeweb/releases/download/v0.2.0/KeeWeb.mac.dmg)
[Linux](https://github.com/antelle/keeweb/releases/download/v0.2.0/KeeWeb.linux.x64.zip)
Timeline: [Release Notes](release-notes.md)
[TODO](TODO.md)
On one page: [Features](features.md)
Twitter: [kee_web](https://twitter.com/kee_web)
# Status
Reading and display is mostly complete; modification and sync is under construction, please see [TODO](TODO.md) for more details.
# Known Issues
These major issues are in progress, or will be fixed in next releases, before v1.0:
- auto-update is not implemented
- dropbox sync is one-way: changes are not loaded from dropbox, only saved
# Self-hosting
Everything you need to host this app on your server is any static file server. The app is a single HTML file + cache manifest (optionally; for offline access).
You can download the latest distribution files from [gh-pages](https://github.com/antelle/keeweb/tree/gh-pages) branch.
# Building
The app can be built with grunt: `grunt` (html file will be in `dist/`) or `grunt watch` (result will be in `tmp/`).
Electron app is built manually, scripts and configs are in `util` directory.
Electron app is built with `grunt electron` (works only under mac osx as it builds dmg; requires wine).
To run Electron app without building, install electron package (`npm install electron-prebuilt -g`) and start with `electron ./electron/`.
# Contributing
Plugins are not supported for now. If you want to add a feature, please contact the author first. Pull requests, patches and issues are very welcome.
If you have found an bug, please [open an issue](https://github.com/antelle/keeweb/issues/new) and fill in the app version and your user-agent
(you can find these details on Settings/Help section).
(you can find these details in Settings/Help section).
# License

45
TODO.md
View File

@ -1,26 +1,37 @@
# MVP
# TODO
- [ ] add/edit groups
## v1.0
# FUTURE
- [ ] trash: groups/empty/untrash
- [ ] move groups/entries
- [x] improve open page UX
- [x] provide engineer error details on file open
- [x] trash: groups/delete/untrash
- [x] move groups/entries
- [ ] auto-update
- [ ] lock flow, auto-lock
- [ ] minimize to tray
- [ ] help/tips
- [ ] switch view
- [ ] optional auto-update
- [ ] lock without closing
- [ ] merge
- [ ] show sync date
- [ ] dropbox keyfiles
- [ ] save to localstorage
- [ ] generation templates
- [x] protected fields
- [x] close files
- [x] offline and local storage
- [x] use dropbox chooser for keyfile
- [ ] trim history by rules
- [ ] custom icons, favicons
- [ ] advanced search
- [ ] mobile
- [ ] sync merge
## Future
- [ ] secure inputs for all passwords
- [ ] keepasshttp
- [ ] mobile apps
- [ ] file type associations
- [ ] auto-type
- [ ] secure fields
- [ ] auto-type for desktop
- [ ] one-time passwords
- [ ] audit
- [ ] generation templates
- [ ] i18n
- [ ] allow to increase font size
- [ ] drag entries across files/to trash
- [ ] external crypto devices support
- [ ] entry templates
- [ ] custom icons
- [ ] plugins

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -11,7 +11,7 @@
</head>
<body class="th-d">
<noscript>
<h1>KeePass Web App</h1>
<h1>KeeWeb</h1>
<p>This app is written entirely in JavaScript. Please, enable JavaScript to run it.</p>
</noscript>
</body>

View File

@ -5,7 +5,9 @@ var AppModel = require('./models/app-model'),
KeyHandler = require('./comp/key-handler'),
Alerts = require('./comp/alerts'),
DropboxLink = require('./comp/dropbox-link'),
Updater = require('./comp/updater');
Updater = require('./comp/updater'),
LastOpenFiles = require('./comp/last-open-files'),
ThemeChanger = require('./util/theme-changer');
$(function() {
require('./mixins/view');
@ -15,7 +17,12 @@ $(function() {
return;
}
KeyHandler.init();
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0) {
var appModel = new AppModel();
if (appModel.settings.get('theme')) {
ThemeChanger.setTheme(appModel.settings.get('theme'));
}
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0 && !localStorage.disableSecurityCheck) {
Alerts.error({ header: 'Not Secure!', icon: 'user-secret', esc: false, enter: false, click: false,
body: 'You have loaded this app with insecure connection. ' +
'Someone may be watching you and stealing your passwords. ' +
@ -32,8 +39,15 @@ $(function() {
}
function showApp() {
var appModel = new AppModel();
new AppView({ model: appModel }).render().showOpenFile(appModel.settings.get('lastOpenFile'));
var appView = new AppView({ model: appModel }).render();
var lastOpenFiles = LastOpenFiles.all();
var lastOpenFile = lastOpenFiles[0];
if (lastOpenFile && lastOpenFile.storage === 'file' && lastOpenFile.path) {
appView.showOpenFile(lastOpenFile.path);
} else {
appView.showOpenFile();
}
Updater.init();
}
});

View File

@ -4,16 +4,7 @@ var Backbone = require('backbone'),
GroupModel = require('../models/group-model');
var GroupCollection = Backbone.Collection.extend({
model: GroupModel,
removeByAttr: function(attr, val) {
var items = this.get('items');
items.forEach(function(item) {
if (item[attr] === val) {
items.remove(item);
}
});
}
model: GroupModel
});
module.exports = GroupCollection;

View File

@ -0,0 +1,7 @@
'use strict';
var DragDropInfo = {
dragObject: null
};
module.exports = DragDropInfo;

View File

@ -7,6 +7,106 @@ var DropboxKeys = {
AppFolder: 'qp7ctun6qt5n9d6'
};
var DropboxChooser = function(callback) {
this.cb = callback;
this.onMessage = this.onMessage.bind(this);
};
DropboxChooser.prototype.callback = function(err, res) {
if (this.cb) {
this.cb(err, res);
}
this.cb = null;
};
DropboxChooser.prototype.choose = function() {
var windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
var url = this.buildUrl();
this.popup = window.open(url, 'dropbox', windowFeatures);
if (!this.popup) {
return this.callback('Failed to open window');
}
window.addEventListener('message', this.onMessage);
this.closeInt = setInterval(this.checkClose.bind(this), 200);
};
DropboxChooser.prototype.buildUrl = function() {
var urlParams = {
origin: encodeURIComponent(window.location.protocol + '//' + window.location.host),
'app_key': DropboxKeys.AppFolder,
'link_type': 'direct',
trigger: 'js',
multiselect: 'false',
extensions: '',
folderselect: 'false',
iframe: 'false',
version: 2
};
return 'https://www.dropbox.com/chooser?' + Object.keys(urlParams).map(function(key) {
return key + '=' + urlParams[key];
}).join('&');
};
DropboxChooser.prototype.onMessage = function(e) {
if (e.source !== this.popup) {
return;
}
var data = JSON.parse(e.data);
switch (data.method) {
case 'origin_request':
e.source.postMessage(JSON.stringify({ method: 'origin' }), 'https://www.dropbox.com');
break;
case 'files_selected':
this.popup.close();
this.success(data.params);
break;
case 'close_dialog':
this.popup.close();
break;
case 'web_session_error':
case 'web_session_unlinked':
this.callback(data.method);
break;
case 'resize':
this.popup.resize(data.params);
break;
case 'error':
this.callback(data.params);
break;
}
};
DropboxChooser.prototype.checkClose = function() {
if (this.popup.closed) {
clearInterval(this.closeInt);
window.removeEventListener('message', this.onMessage);
if (!this.result) {
this.callback('closed');
}
}
};
DropboxChooser.prototype.success = function(params) {
/* jshint camelcase:false */
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
return this.callback('bad result');
}
this.result = params[0];
this.readFile(this.result.link);
};
DropboxChooser.prototype.readFile = function(url) {
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', (function() {
this.callback(null, { name: this.result.name, data: xhr.response });
}).bind(this));
xhr.addEventListener('error', this.callback.bind(this, 'download error'));
xhr.addEventListener('abort', this.callback.bind(this, 'download abort'));
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();
};
var DropboxLink = {
_getClient: function(complete) {
if (this._dropboxClient && this._dropboxClient.isAuthenticated()) {
@ -23,7 +123,11 @@ var DropboxLink = {
}).bind(this));
},
_handleUiError: function(err, callback) {
_handleUiError: function(err, alertCallback, callback) {
if (!alertCallback) {
alertCallback = Alerts.error.bind(Alerts);
}
console.error('Dropbox error', err);
switch (err.status) {
case Dropbox.ApiError.INVALID_TOKEN:
Alerts.yesno({
@ -38,25 +142,25 @@ var DropboxLink = {
});
return;
case Dropbox.ApiError.NOT_FOUND:
Alerts.error({
alertCallback({
header: 'Dropbox Sync Error',
body: 'The file was not found. Has it been removed from another computer?'
});
break;
case Dropbox.ApiError.OVER_QUOTA:
Alerts.error({
alertCallback({
header: 'Dropbox Full',
body: 'Your Dropbox is full, there\'s no space left anymore.'
});
break;
case Dropbox.ApiError.RATE_LIMITED:
Alerts.error({
alertCallback({
header: 'Dropbox Sync Error',
body: 'Too many requests to Dropbox have been made by this app. Please, try again later.'
});
break;
case Dropbox.ApiError.NETWORK_ERROR:
Alerts.error({
alertCallback({
header: 'Dropbox Sync Network Error',
body: 'Network error occured during Dropbox sync. Please, check your connection and try again.'
});
@ -64,13 +168,13 @@ var DropboxLink = {
case Dropbox.ApiError.INVALID_PARAM:
case Dropbox.ApiError.OAUTH_ERROR:
case Dropbox.ApiError.INVALID_METHOD:
Alerts.error({
alertCallback({
header: 'Dropbox Sync Error',
body: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ' + err.status
});
break;
default:
Alerts.error({
alertCallback({
header: 'Dropbox Sync Error',
body: 'Something went wrong during Dropbox sync. Please, try again later. Error: ' + err
});
@ -79,7 +183,7 @@ var DropboxLink = {
callback(false);
},
_callAndHandleError: function(callName, args, callback) {
_callAndHandleError: function(callName, args, callback, errorAlertCallback) {
var that = this;
this._getClient(function(err, client) {
if (err) {
@ -87,9 +191,9 @@ var DropboxLink = {
}
client[callName].apply(client, args.concat(function(err, res) {
if (err) {
that._handleUiError(err, function(repeat) {
that._handleUiError(err, errorAlertCallback, function(repeat) {
if (repeat) {
that._callAndHandleError(callName, args, callback);
that._callAndHandleError(callName, args, callback, errorAlertCallback);
} else {
callback(err);
}
@ -122,8 +226,8 @@ var DropboxLink = {
}
},
openFile: function(fileName, complete) {
this._callAndHandleError('readFile', [fileName, { blob: true }], complete);
openFile: function(fileName, complete, errorAlertCallback) {
this._callAndHandleError('readFile', [fileName, { blob: true }], complete, errorAlertCallback);
},
getFileList: function(complete) {
@ -133,6 +237,10 @@ var DropboxLink = {
}
complete(err, files);
});
},
chooseFile: function(callback) {
new DropboxChooser(callback).choose();
}
};

View File

@ -0,0 +1,55 @@
'use strict';
var Storage = require('./storage');
var MaxItems = 5;
var LastOpenFiles = {
all: function() {
try {
return JSON.parse(localStorage.lastOpenFiles).map(function(f) {
f.dt = Date.parse(f.dt);
return f;
});
} catch (e) {
return [];
}
},
byName: function(name) {
return this.all().filter(function(f) { return f.name === name; })[0];
},
save: function(files) {
try {
localStorage.lastOpenFiles = JSON.stringify(files);
} catch (e) {
console.error('Error saving last open files', e);
}
},
add: function(name, storage, path, availOffline) {
console.log('Add last open file', name, storage, path);
var files = this.all();
files = files.filter(function(f) { return f.name !== name; });
files.unshift({ name: name, storage: storage, path: path, availOffline: availOffline, dt: new Date() });
while (files.length > MaxItems) {
files.pop();
}
this.save(files);
},
remove: function(name) {
console.log('Remove last open file', name);
var files = this.all();
files.forEach(function(file) {
if (file.name === name && file.availOffline) {
Storage.cache.remove(file.name);
}
}, this);
files = files.filter(function(file) { return file.name !== name; });
this.save(files);
}
};
module.exports = LastOpenFiles;

View File

@ -9,6 +9,9 @@ if (window.process && window.process.versions && window.process.versions.electro
name: 'electron',
version: window.process.versions.electron,
req: window.require,
remReq: function(mod) {
return this.req('remote').require(mod);
},
openLink: function(href) {
this.req('shell').openExternal(href);
},
@ -17,12 +20,11 @@ if (window.process && window.process.versions && window.process.versions.electro
this.req('remote').getCurrentWindow().openDevTools();
},
getSaveFileName: function(defaultPath, cb) {
var remote = this.req('remote');
if (defaultPath) {
var homePath = remote.require('app').getPath('userDesktop');
var homePath = this.remReq('app').getPath('userDesktop');
defaultPath = this.req('path').join(homePath, defaultPath);
}
remote.require('dialog').showSaveDialog({
this.remReq('dialog').showSaveDialog({
title: 'Save Passwords Database',
defaultPath: defaultPath,
filters: [{ name: 'KeePass files', extensions: ['kdbx'] }]
@ -32,20 +34,29 @@ if (window.process && window.process.versions && window.process.versions.electro
var path = this.req('path').join(this.req('remote').require('app').getPath('userData'), 'index.html');
this.writeFile(path, data);
},
getUserDataPath: function(fileName) {
return this.req('path').join(this.remReq('app').getPath('userData'), fileName || '');
},
writeFile: function(path, data) {
this.req('fs').writeFileSync(path, new window.Buffer(data));
},
readFile: function(path) {
return new Uint8Array(this.req('fs').readFileSync(path));
readFile: function(path, encoding) {
var contents = this.req('fs').readFileSync(path, encoding);
return typeof contents === 'string' ? contents : new Uint8Array(contents);
},
fileExists: function(path) {
return this.req('fs').existsSync(path);
},
exit: function() {
Launcher.exitRequested = true;
this.remReq('app').quit();
}
};
window.launcherOpen = function(path) {
Backbone.trigger('launcher-open-file', path);
};
if (window.launcherOpenedFile) {
console.log('Open file request', window.launcherOpenedFile);
Backbone.trigger('launcher-open-file', window.launcherOpenedFile);
delete window.launcherOpenedFile;
}

View File

@ -19,6 +19,12 @@ SecureInput.prototype.reset = function() {
this.el = null;
this.length = 0;
this.pseudoValue = '';
if (this.salt) {
for (var i = 0; i < this.salt.length; i++) {
this.salt[i] = 0;
}
}
this.salt = new Uint32Array(0);
};

View File

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

View File

@ -0,0 +1,114 @@
'use strict';
var idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
var StorageCache = {
name: 'cache',
enabled: !!idb,
db: null,
errorOpening: null,
init: function(callback) {
if (this.db) {
return callback();
}
var that = this;
try {
var req = idb.open('FilesCache');
req.onerror = function (e) {
console.error('Error opening indexed db', e);
that.errorOpening = e;
callback(e);
};
req.onsuccess = function (e) {
that.db = e.target.result;
callback();
};
req.onupgradeneeded = function (e) {
var db = e.target.result;
db.createObjectStore('files');
};
} catch (e) {
console.error('Error opening indexed db', e);
callback(e);
}
},
save: function(id, data, callback) {
this.init((function(err) {
if (err) {
return callback(err);
}
try {
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
req.onsuccess = function () {
if (callback) {
callback();
}
};
req.onerror = function () {
console.error('Error saving to cache', id, req.error);
if (callback) {
callback(req.error);
}
};
} catch (e) {
console.error('Error saving to cache', id, e);
callback(e);
}
}).bind(this));
},
load: function(id, callback) {
this.init((function(err) {
if (err) {
return callback(null, err);
}
try {
var req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
req.onsuccess = function () {
if (callback) {
callback(req.result);
}
};
req.onerror = function () {
console.error('Error loading from cache', id, req.error);
if (callback) {
callback(null, req.error);
}
};
} catch (e) {
console.error('Error loading from cache', id, e);
callback(null, e);
}
}).bind(this));
},
remove: function(id, callback) {
this.init((function(err) {
if (err) {
return callback(err);
}
try {
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
req.onsuccess = function () {
if (callback) {
callback();
}
};
req.onerror = function () {
console.error('Error removing from cache', id, req.error);
if (callback) {
callback(req.error);
}
};
} catch(e) {
console.error('Error removing from cache', id, e);
callback(e);
}
}).bind(this));
}
};
module.exports = StorageCache;

View File

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

View File

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

View File

@ -1,6 +1,7 @@
'use strict';
var RuntimeInfo = require('./runtime-info'),
var Backbone = require('backbone'),
RuntimeInfo = require('./runtime-info'),
Links = require('../const/links'),
Launcher = require('../comp/launcher'),
AppSettingsModel = require('../models/app-settings-model'),
@ -87,7 +88,7 @@ var Updater = {
// TODO: potential DDoS in case on any error! Save file with version and check before the download
UpdateModel.instance.set('updateStatus', 'downloading');
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', (function(e) {
xhr.addEventListener('load', (function() {
if (xhr.response.byteLength > this.MinUpdateSize) {
UpdateModel.instance.set('updateStatus', 'downloaded');
try {

View File

@ -18,6 +18,7 @@ var Resizable = {
var size = dragInfo.startSize + e.offset;
size = Math.max(dragInfo.min, Math.min(dragInfo.max, size));
this.$el[dragInfo.prop](size);
this.trigger('view-resize', size);
Backbone.trigger('page-geometry', { source: 'resizable' });
},
@ -29,6 +30,7 @@ var Resizable = {
this.$el.css(dragInfo.prop, 'auto');
}
this.fixSize(dragInfo);
this.trigger('view-resize', null);
Backbone.trigger('page-geometry', { source: 'resizable' });
},

View File

@ -22,6 +22,10 @@ _.extend(Backbone.View.prototype, {
return this;
},
isHidden: function() {
return this._hidden;
},
afterPaint: function(callback) {
this.requestAnimationFrame(function() {
this.requestAnimationFrame(callback);

View File

@ -4,6 +4,7 @@ var Backbone = require('backbone'),
AppSettingsModel = require('./app-settings-model'),
MenuModel = require('./menu/menu-model'),
EntryModel = require('./entry-model'),
GroupModel = require('./group-model'),
FileCollection = require('../collections/file-collection'),
EntryCollection = require('../collections/entry-collection');
@ -22,6 +23,8 @@ var AppModel = Backbone.Model.extend({
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);
},
addFile: function(file) {
@ -35,9 +38,6 @@ var AppModel = Backbone.Model.extend({
page: 'file',
file: file
});
if (file.get('path')) {
AppSettingsModel.instance.set('lastOpenFile', file.get('path'));
}
this.refresh();
},
@ -93,8 +93,24 @@ var AppModel = Backbone.Model.extend({
this.setFilter({});
},
closeFile: function(file) {
this.files.remove(file);
this._tagsChanged();
this.menu.groupsSection.removeByFile(file);
this.menu.filesSection.removeByFile(file);
this.refresh();
},
emptyTrash: function() {
this.files.forEach(function(file) {
file.emptyTrash();
}, this);
this.refresh();
},
setFilter: function(filter) {
this.filter = filter;
this.filter.subGroups = this.settings.get('expandGroups');
var entries = this.getEntries();
Backbone.trigger('filter', { filter: this.filter, sort: this.sort, entries: entries });
Backbone.trigger('select-entry', entries.length ? entries.first() : null);
@ -122,12 +138,26 @@ var AppModel = Backbone.Model.extend({
});
});
entries.sortEntries(this.sort);
if (this.filter.trash) {
this.addTrashGroups(entries);
}
if (entries.length) {
entries.setActive(entries.first());
}
return entries;
},
addTrashGroups: function(collection) {
this.files.forEach(function(file) {
var trashGroup = file.getTrashGroup();
if (trashGroup) {
trashGroup.getOwnSubGroups().forEach(function(group) {
collection.unshift(GroupModel.fromGroup(group, file, trashGroup));
});
}
});
},
prepareFilter: function() {
var filter = _.clone(this.filter);
if (filter.text) {
@ -139,7 +169,7 @@ var AppModel = Backbone.Model.extend({
return filter;
},
createNewEntry: function() {
getFirstSelectedGroup: function() {
var selGroupId = this.filter.group;
var file, group;
if (selGroupId) {
@ -155,7 +185,17 @@ var AppModel = Backbone.Model.extend({
file = this.files.first();
group = file.get('groups').first();
}
return EntryModel.newEntry(group, file);
return { group: group, file: file };
},
createNewEntry: function() {
var sel = this.getFirstSelectedGroup();
return EntryModel.newEntry(sel.group, sel.file);
},
createNewGroup: function() {
var sel = this.getFirstSelectedGroup();
return GroupModel.newGroup(sel.group, sel.file);
}
});

View File

@ -1,11 +1,16 @@
'use strict';
var Backbone = require('backbone');
var Backbone = require('backbone'),
Launcher = require('../comp/launcher');
var FileName = 'app-settings.json';
var AppSettingsModel = Backbone.Model.extend({
defaults: {
theme: 'd',
lastOpenFile: '',
expandGroups: true,
listViewWidth: null,
menuViewWidth: null,
autoUpdate: true
},
@ -14,17 +19,30 @@ var AppSettingsModel = Backbone.Model.extend({
},
load: function() {
if (typeof localStorage !== 'undefined' && localStorage.appSettings) {
try {
var data = JSON.parse(localStorage.appSettings);
this.set(data, { silent: true });
} catch (e) { /* failed to load settings */ }
try {
var data;
if (Launcher) {
data = JSON.parse(Launcher.readFile(Launcher.getUserDataPath(FileName), '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);
}
},
save: function() {
if (typeof localStorage !== 'undefined') {
localStorage.appSettings = JSON.stringify(this.attributes);
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);
}
}
});

View File

@ -20,14 +20,13 @@ var EntryModel = Backbone.Model.extend({
this.group = group;
this.file = file;
this._fillByEntry();
this._fillInTrash();
},
_fillByEntry: function() {
var entry = this.entry;
this.fileName = this.file.db.meta.name;
this.title = entry.fields.Title || '';
this.password = entry.fields.Password;
this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
this.notes = entry.fields.Notes || '';
this.url = entry.fields.URL || '';
this.user = entry.fields.UserName || '';
@ -91,19 +90,6 @@ var EntryModel = Backbone.Model.extend({
return att;
},
_fillInTrash: function() {
this.deleted = false;
if (this.file.db.meta.recycleBinEnabled) {
var trashGroupId = this.file.db.meta.recycleBinUuid.id;
for (var group = this.group; group; group = group.group) {
if (group.id === trashGroupId) {
this.deleted = true;
break;
}
}
}
},
_entryModified: function() {
if (!this.unsaved) {
this.unsaved = true;
@ -149,7 +135,8 @@ var EntryModel = Backbone.Model.extend({
setField: function(field, val) {
this._entryModified();
if (val || this.buildInFields.indexOf(field) >= 0) {
var hasValue = val && (typeof val === 'string' || val instanceof kdbxweb.ProtectedValue && val.byteLength);
if (hasValue || this.buildInFields.indexOf(field) >= 0) {
this.entry.fields[field] = val;
} else {
delete this.entry.fields[field];
@ -217,16 +204,20 @@ var EntryModel = Backbone.Model.extend({
},
moveToTrash: function() {
this.file.db.remove(this.entry, this.group.group);
this.file.db.remove(this.entry);
this.group.removeEntry(this);
var trashGroup = this.file.getTrashGroup();
if (trashGroup) {
trashGroup.addEntry(this);
this.group = trashGroup;
this.deleted = true;
}
},
deleteFromTrash: function() {
this.file.db.move(this.entry, null);
this.group.removeEntry(this);
},
removeWithoutHistory: function() {
var ix = this.group.group.entries.indexOf(this.entry);
if (ix >= 0) {

View File

@ -5,6 +5,8 @@ var Backbone = require('backbone'),
GroupModel = require('./group-model'),
Launcher = require('../comp/launcher'),
DropboxLink = require('../comp/dropbox-link'),
Storage = require('../comp/storage'),
LastOpenFiles = require('../comp/last-open-files'),
kdbxweb = require('kdbxweb'),
demoFileData = require('base64!../../resources/Demo.kdbx');
@ -26,7 +28,9 @@ var FileModel = Backbone.Model.extend({
oldKeyFileName: '',
passwordChanged: false,
keyFileChanged: false,
syncing: false
syncing: false,
availOffline: false,
offline: false
},
db: null,
@ -51,13 +55,53 @@ 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);
this.db = kdbxweb.Kdbx.load(fileData, credentials);
var start = performance.now();
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);
} else {
this.db = db;
this.readModel(this.get('name'));
this.setOpenFile({ passwordLength: len });
if (keyFileData) {
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
}
console.log('Opened file ' + this.get('name') + ': ' + Math.round(performance.now() - start) + 'ms, ' +
db.header.keyEncryptionRounds + ' rounds, ' + Math.round(fileData.byteLength / 1024) + ' kB');
this.postOpen(fileData);
}
}).bind(this));
} catch (e) {
console.error('Error opening file', e, e.code, e.message, e);
this.set({ error: true, opening: false });
return;
}
this.readModel(this.get('name'));
this.setOpenFile({ passwordLength: len });
},
postOpen: function(fileData) {
var that = this;
if (!this.get('offline')) {
if (this.get('availOffline')) {
Storage.cache.save(this.get('name'), fileData, function (err) {
if (err) {
that.set('availOffline', false);
if (!that.get('storage')) {
return;
}
}
that.addToLastOpenFiles(!err);
});
} else {
if (this.get('storage')) {
this.addToLastOpenFiles(false);
}
Storage.cache.remove(this.get('name'));
}
}
},
addToLastOpenFiles: function(hasOfflineCache) {
LastOpenFiles.add(this.get('name'), this.get('storage'), this.get('path'), hasOfflineCache);
},
create: function(name) {
@ -65,16 +109,18 @@ var FileModel = Backbone.Model.extend({
var credentials = new kdbxweb.Credentials(password);
this.db = kdbxweb.Kdbx.create(credentials, name);
this.readModel();
this.set({ open: true, created: true, opening: false, error: false, name: name });
this.set({ open: true, created: true, opening: false, error: false, name: name, offline: false });
},
createDemo: function() {
var password = kdbxweb.ProtectedValue.fromString('demo');
var credentials = new kdbxweb.Credentials(password);
var demoFile = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(demoFileData));
this.db = kdbxweb.Kdbx.load(demoFile, credentials);
this.readModel();
this.setOpenFile({ passwordLength: 4, demo: true, name: 'Demo' });
kdbxweb.Kdbx.load(demoFile, credentials, (function(db) {
this.db = db;
this.readModel();
this.setOpenFile({passwordLength: 4, demo: true, name: 'Demo'});
}).bind(this));
},
setOpenFile: function(props) {
@ -136,9 +182,11 @@ var FileModel = Backbone.Model.extend({
if (top.forEachOwnEntry) {
top.forEachOwnEntry(filter, callback);
}
top.forEachGroup(function (group) {
group.forEachOwnEntry(filter, callback);
});
if (!filter.group || filter.subGroups) {
top.forEachGroup(function (group) {
group.forEachOwnEntry(filter, callback);
});
}
}
},
@ -161,30 +209,37 @@ var FileModel = Backbone.Model.extend({
},
autoSave: function() {
this.set('syncing', true);
switch (this.get('storage')) {
var that = this;
that.set('syncing', true);
switch (that.get('storage')) {
case 'file':
Launcher.writeFile(this.get('path'), this.getData());
this.saved(this.get('path'), this.get('storage'));
that.getData(function(data) {
Launcher.writeFile(that.get('path'), data);
that.saved(that.get('path'), that.get('storage'));
});
break;
case 'dropbox':
DropboxLink.saveFile(this.get('path'), this.getData(), true, (function(err) {
if (!err) {
this.saved(this.get('path'), this.get('storage'));
}
}).bind(this));
that.getData(function(data) {
DropboxLink.saveFile(that.get('path'), data, true, function (err) {
if (!err) {
that.saved(that.get('path'), that.get('storage'));
}
});
});
break;
default:
throw 'Unknown storage; cannot auto save';
}
},
getData: function() {
return this.db.save();
getData: function(cb) {
var data = this.db.save(cb);
return data;
},
getXml: function() {
return this.db.saveXml();
getXml: function(cb) {
this.db.saveXml(cb);
},
saved: function(path, storage) {
@ -193,6 +248,7 @@ var FileModel = Backbone.Model.extend({
this.forEachEntry({}, function(entry) {
entry.unsaved = false;
});
this.addToLastOpenFiles();
},
setPassword: function(password) {
@ -245,6 +301,7 @@ var FileModel = Backbone.Model.extend({
this.db.meta.name = name;
this.db.meta.nameChanged = new Date();
this.set('name', name);
this.get('groups').first().setName(name);
this.setModified();
},
@ -281,6 +338,19 @@ var FileModel = Backbone.Model.extend({
this.db.header.keyEncryptionRounds = rounds;
this.set('keyEncryptionRounds', rounds);
this.setModified();
},
emptyTrash: function() {
var trashGroup = this.getTrashGroup();
if (trashGroup) {
trashGroup.getOwnSubGroups().slice().forEach(function(group) {
this.db.move(group, null);
}, this);
trashGroup.group.entries.forEach(function(entry) {
this.db.move(entry, null);
}, this);
trashGroup.get('entries').reset();
}
}
});

View File

@ -10,7 +10,11 @@ var GroupModel = MenuItemModel.extend({
defaults: _.extend({}, MenuItemModel.prototype.defaults, {
iconId: 0,
entries: null,
filterKey: 'group'
filterKey: 'group',
editable: true,
top: false,
drag: true,
drop: true
}),
initialize: function() {
@ -23,9 +27,6 @@ var GroupModel = MenuItemModel.extend({
var isRecycleBin = file.db.meta.recycleBinUuid && file.db.meta.recycleBinUuid.id === group.uuid.id;
this.set({
id: group.uuid.id,
title: group.name,
iconId: group.icon,
icon: this._iconFromId(group.icon),
expanded: true,
visible: !isRecycleBin,
items: new GroupCollection(),
@ -33,16 +34,25 @@ var GroupModel = MenuItemModel.extend({
}, { silent: true });
this.group = group;
this.file = file;
this._fillByGroup(true);
var items = this.get('items'),
entries = this.get('entries');
group.groups.forEach(function(subGroup) {
items.add(GroupModel.fromGroup(subGroup, file));
});
items.add(GroupModel.fromGroup(subGroup, file, this));
}, this);
group.entries.forEach(function(entry) {
entries.add(EntryModel.fromEntry(entry, this, file));
}, this);
},
_fillByGroup: function(silent) {
this.set({
title: this.group.name,
iconId: this.group.icon,
icon: this._iconFromId(this.group.icon)
}, { silent: silent });
},
_iconFromId: function(id) {
if (id === KdbxIcons.Folder || id === KdbxIcons.FolderOpen) {
return undefined;
@ -50,6 +60,14 @@ var GroupModel = MenuItemModel.extend({
return IconMap[id];
},
_groupModified: function() {
this.file.setModified();
if (this.isJustCreated) {
this.isJustCreated = false;
}
this.group.times.update();
},
forEachGroup: function(callback, includeDisabled) {
var result = true;
this.get('items').forEach(function(group) {
@ -68,18 +86,110 @@ var GroupModel = MenuItemModel.extend({
});
},
getOwnSubGroups: function() {
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) {
this._groupModified();
this.group.name = name;
this._fillByGroup();
},
setIcon: function(iconId) {
this._groupModified();
this.group.icon = iconId;
this._fillByGroup();
},
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');
},
deleteFromTrash: function() {
this.file.db.move(this.group, null);
this.parentGroup.removeGroup(this);
},
removeWithoutHistory: function() {
var ix = this.parentGroup.group.groups.indexOf(this.group);
if (ix >= 0) {
this.parentGroup.group.groups.splice(ix, 1);
}
this.parentGroup.removeGroup(this);
this.trigger('delete');
},
moveHere: function(object) {
if (!object || object.id === this.id || object.file !== this.file) {
return;
}
if (object instanceof GroupModel) {
if (this.group.groups.indexOf(object.group) >= 0) {
return;
}
this.file.db.move(object.group, this.group);
object.parentGroup.removeGroup(object);
object.trigger('delete');
this.addGroup(object);
} 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);
}
}
});
GroupModel.fromGroup = function(group, file) {
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 });
}
return model;
};
GroupModel.newGroup = function(group, file) {
var model = new GroupModel();
var grp = file.db.createGroup(group.group);
model.setFromGroup(grp, file);
model.group.times.update();
model.parentGroup = group;
model.unsaved = true;
model.isJustCreated = true;
group.addGroup(model);
file.setModified();
return model;
};

View File

@ -16,6 +16,8 @@ var MenuItemModel = Backbone.Model.extend({
cls: null,
disabled: false,
visible: true,
drag: false,
drop: false,
filterKey: null,
filterValue: null
},

View File

@ -28,7 +28,16 @@ var MenuItemModel = Backbone.Model.extend({
},
removeByFile: function(file) {
this.get('items').removeByAttr('file', file);
var items = this.get('items');
var toRemove;
items.each(function(item) {
if (item.file === file || item.get('file') === file) {
toRemove = item;
}
});
if (toRemove) {
items.remove(toRemove);
}
this.trigger('change-items');
},

View File

@ -23,7 +23,7 @@ var UpdateModel = Backbone.Model.extend({
var data = JSON.parse(localStorage.updateInfo);
_.each(data, function(val, key) {
if (/Date$/.test(key)) {
data[key] = val ? new Date(val) : null
data[key] = val ? new Date(val) : null;
}
});
this.set(data, { silent: true });

View File

@ -8,20 +8,29 @@ var EntryPresenter = function(descField) {
};
EntryPresenter.prototype = {
present: function(entry) { this.entry = entry; return this; },
get id() { return this.entry.id; },
get icon() { return this.entry.icon; },
get color() { return this.entry.color; },
get title() { return this.entry.title; },
get notes() { return this.entry.notes; },
get url() { return this.entry.url; },
get user() { return this.entry.user; },
get active() { return this.entry.active; },
get created() { return Format.dtStr(this.entry.created); },
get updated() { return Format.dtStr(this.entry.updated); },
get expired() { return this.entry.expired; },
get deleted() { return this.entry.deleted; },
present: function(item) {
if (item.entry) {
this.entry = item;
} else {
this.group = item;
}
return this;
},
get id() { return this.entry ? this.entry.id : this.group.get('id'); },
get icon() { return this.entry ? this.entry.icon : this.group.get('icon'); },
get color() { return this.entry ? this.entry.color : 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 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; },
get description() {
if (!this.entry) {
return '[Group]';
}
switch (this.descField) {
case 'website':
return this.url || '(no website)';

View File

@ -0,0 +1,14 @@
'use strict';
var ThemeChanger = {
setTheme: function(theme) {
_.forEach(document.body.classList, function(cls) {
if (/^th\-/.test(cls)) {
document.body.classList.remove(cls);
}
});
document.body.classList.add('th-' + theme);
}
};
module.exports = ThemeChanger;

View File

@ -6,12 +6,14 @@ var Backbone = require('backbone'),
FooterView = require('../views/footer-view'),
ListView = require('../views/list-view'),
DetailsView = require('../views/details/details-view'),
GrpView = require('../views/grp-view'),
OpenView = require('../views/open-view'),
SettingsView = require('../views/settings/settings-view'),
Alerts = require('../comp/alerts'),
Keys = require('../const/keys'),
KeyHandler = require('../comp/key-handler'),
Launcher = require('../comp/launcher');
Launcher = require('../comp/launcher'),
ThemeChanger = require('../util/theme-changer');
var AppView = Backbone.View.extend({
el: 'body',
@ -36,6 +38,7 @@ var AppView = Backbone.View.extend({
this.views.listDrag = new DragView('x');
this.views.details = new DetailsView();
this.views.details.appModel = this.model;
this.views.grp = new GrpView();
this.views.menu.listenDrag(this.views.menuDrag);
this.views.list.listenDrag(this.views.listDrag);
@ -43,6 +46,7 @@ var AppView = Backbone.View.extend({
this.listenTo(this.model.settings, 'change:theme', this.setTheme);
this.listenTo(this.model.files, 'update reset', this.fileListUpdated);
this.listenTo(Backbone, 'select-all', this.selectAll);
this.listenTo(Backbone, 'menu-select', this.menuSelect);
this.listenTo(Backbone, 'lock-workspace', this.lockWorkspace);
this.listenTo(Backbone, 'show-file', this.showFileSettings);
@ -52,6 +56,7 @@ var AppView = Backbone.View.extend({
this.listenTo(Backbone, 'toggle-settings', this.toggleSettings);
this.listenTo(Backbone, 'toggle-menu', this.toggleMenu);
this.listenTo(Backbone, 'toggle-details', this.toggleDetails);
this.listenTo(Backbone, 'edit-group', this.editGroup);
this.listenTo(Backbone, 'launcher-open-file', this.launcherOpenFile);
this.listenTo(Backbone, 'update-app', this.updateApp);
@ -71,6 +76,7 @@ var AppView = Backbone.View.extend({
this.views.list.setElement(this.$el.find('.app__list')).render();
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();
return this;
},
@ -80,6 +86,7 @@ var AppView = Backbone.View.extend({
this.views.list.hide();
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.hide();
this.views.footer.toggle(this.model.files.hasOpenFiles());
this.hideSettings();
this.hideOpenFile();
@ -99,9 +106,9 @@ var AppView = Backbone.View.extend({
updateApp: function() {
if (this.model.files.hasOpenFiles()) {
// TODO: show update bubble
this.showUpdateBubble(); // TODO
} else {
this.location.reload();
window.location.reload();
}
},
@ -111,6 +118,7 @@ var AppView = Backbone.View.extend({
this.views.list.show();
this.views.listDrag.show();
this.views.details.show();
this.views.grp.hide();
this.views.footer.show();
this.hideOpenFile();
this.hideSettings();
@ -138,6 +146,7 @@ var AppView = Backbone.View.extend({
this.views.list.hide();
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.hide();
this.hideOpenFile();
this.views.settings = new SettingsView();
this.views.settings.setElement(this.$el.find('.app__body')).render();
@ -148,6 +157,13 @@ var AppView = Backbone.View.extend({
this.views.menu.switchVisibility(false);
},
showEditGroup: function() {
this.views.list.hide();
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.show();
},
fileListUpdated: function() {
if (this.model.files.hasOpenFiles()) {
this.showEntries();
@ -179,8 +195,20 @@ var AppView = Backbone.View.extend({
}
},
beforeUnload: function() {
beforeUnload: function(e) {
if (this.model.files.hasUnsavedFiles()) {
if (Launcher && !Launcher.exitRequested) {
Alerts.yesno({
header: 'Unsaved changes!',
body: 'You have unsaved files, all changes will be lost.',
buttons: [{result: 'yes', title: 'Exit and discard unsaved changes'}, {result: '', title: 'Don\'t exit'}],
success: function() {
Launcher.exit();
}
});
e.returnValue = false;
return false;
}
return 'You have unsaved files, all changes will be lost.';
}
},
@ -201,8 +229,15 @@ var AppView = Backbone.View.extend({
}
},
menuSelect: function(item) {
this.model.menu.select(item);
selectAll: function() {
this.menuSelect({ item: this.model.menu.allItemsSection.get('items').first() });
},
menuSelect: function(opt) {
this.model.menu.select(opt);
if (!this.views.grp.isHidden()) {
this.showEntries();
}
},
lockWorkspace: function() {
@ -224,6 +259,7 @@ var AppView = Backbone.View.extend({
try {
file.autoSave();
} catch (e) {
console.error('Failed to auto-save file', file.get('path'), e);
fileId = file.cid;
}
} else if (!fileId) {
@ -269,6 +305,15 @@ var AppView = Backbone.View.extend({
this.views.menu.switchVisibility(false);
},
editGroup: function(group) {
if (group && this.views.grp.isHidden()) {
this.showEditGroup();
this.views.grp.showGroup(group);
} else {
this.showEntries();
}
},
contextmenu: function(e) {
if (['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) < 0) {
e.preventDefault();
@ -284,13 +329,7 @@ var AppView = Backbone.View.extend({
},
setTheme: function() {
var theme = this.model.settings.get('theme');
_.forEach(document.body.classList, function(cls) {
if (/^th\-/.test(cls)) {
document.body.classList.remove(cls);
}
});
document.body.classList.add('th-' + theme);
ThemeChanger.setTheme(this.model.settings.get('theme'));
},
extLinkClick: function(e) {

View File

@ -1,6 +1,7 @@
'use strict';
var Backbone = require('backbone'),
GroupModel = require('../../models/group-model'),
Scrollable = require('../../mixins/scrollable'),
FieldViewText = require('../fields/field-view-text'),
FieldViewDate = require('../fields/field-view-date'),
@ -9,11 +10,12 @@ var Backbone = require('backbone'),
FieldViewReadOnly = require('../fields/field-view-read-only'),
FieldViewHistory = require('../fields/field-view-history'),
FieldViewCustom = require('../fields/field-view-custom'),
DetailsIconView = require('./details-icon-view'),
IconSelectView = require('../icon-select-view'),
DetailsHistoryView = require('./details-history-view'),
DetailsAttachmentView = require('./details-attachment-view'),
Keys = require('../../const/keys'),
KeyHandler = require('../../comp/key-handler'),
Alerts = require('../../comp/alerts'),
CopyPaste = require('../../util/copy-paste'),
Format = require('../../util/format'),
FileSaver = require('filesaver'),
@ -23,6 +25,7 @@ var Backbone = require('backbone'),
var DetailsView = Backbone.View.extend({
template: require('templates/details/details.html'),
emptyTemplate: require('templates/details/details-empty.html'),
groupTemplate: require('templates/details/details-group.html'),
fieldViews: null,
views: null,
@ -34,6 +37,7 @@ var DetailsView = Backbone.View.extend({
'click .details__header-title': 'editTitle',
'click .details__history-link': 'showHistory',
'click .details__buttons-trash': 'moveToTrash',
'click .details__buttons-trash-del': 'deleteFromTrash',
'click .details__back-button': 'backClick',
'dragover .details': 'dragover',
'dragleave .details': 'dragleave',
@ -73,7 +77,12 @@ var DetailsView = Backbone.View.extend({
this.$el.html(this.emptyTemplate());
return;
}
this.$el.html(this.template(this.model));
if (this.model instanceof GroupModel) {
this.$el.html(this.groupTemplate());
return;
}
var model = $.extend({ deleted: this.appModel.filter.trash }, this.model);
this.$el.html(this.template(model));
this.setSelectedColor(this.model.color);
this.addFieldViews();
this.scroll = baron({
@ -98,7 +107,7 @@ var DetailsView = Backbone.View.extend({
var model = this.model;
this.fieldViews.push(new FieldViewText({ model: { name: '$UserName', title: 'User',
value: function() { return model.user; } } }));
this.fieldViews.push(new FieldViewText({ model: { name: '$Password', title: 'Password',
this.fieldViews.push(new FieldViewText({ model: { name: '$Password', title: 'Password', canGen: true,
value: function() { return model.password; } } }));
this.fieldViews.push(new FieldViewUrl({ model: { name: '$URL', title: 'Website',
value: function() { return model.url; } } }));
@ -175,12 +184,12 @@ var DetailsView = Backbone.View.extend({
},
toggleIcons: function() {
if (this.views.sub && this.views.sub instanceof DetailsIconView) {
if (this.views.sub && this.views.sub instanceof IconSelectView) {
this.render();
return;
}
this.removeSubView();
var subView = new DetailsIconView({ el: this.scroller, model: this.model });
var subView = new IconSelectView({ el: this.scroller, model: this.model });
this.listenTo(subView, 'select', this.iconSelected);
subView.render();
this.pageResized();
@ -398,11 +407,6 @@ var DetailsView = Backbone.View.extend({
},
setTitle: function(title) {
if (!title && this.model.isJustCreated) {
this.model.removeWithoutHistory();
Backbone.trigger('refresh');
return;
}
if (this.model.title instanceof kdbxweb.ProtectedValue) {
title = kdbxweb.ProtectedValue.fromString(title);
}
@ -410,7 +414,7 @@ var DetailsView = Backbone.View.extend({
this.model.setField('Title', title);
this.entryUpdated(true);
}
var newTitle = $('<h1 class="details__header-title"></h1>').text(title);
var newTitle = $('<h1 class="details__header-title"></h1>').text(title || '(no title)');
this.$el.find('.details__header-title-input').replaceWith(newTitle);
},
@ -465,6 +469,18 @@ var DetailsView = Backbone.View.extend({
Backbone.trigger('refresh');
},
deleteFromTrash: function() {
Alerts.yesno({
header: 'Delete from trash?',
body: 'You will not be able to put it back<p class="muted-color">To quickly remove all items from trash, click empty icon in Trash menu</p>',
icon: 'minus-circle',
success: (function() {
this.model.deleteFromTrash();
Backbone.trigger('refresh');
}).bind(this)
});
},
backClick: function() {
Backbone.trigger('toggle-details', false);
}

View File

@ -1,11 +1,13 @@
'use strict';
var FieldViewText = require('./field-view-text'),
Keys = require('../../const/keys');
FieldView = require('./field-view'),
Keys = require('../../const/keys'),
kdbxweb = require('kdbxweb');
var FieldViewCustom = FieldViewText.extend({
events: {
'mousedown .details__field-label': 'fieldLabelMousedown',
'mousedown .details__field-label': 'fieldLabelMousedown'
},
initialize: function() {
@ -19,6 +21,13 @@ var FieldViewCustom = FieldViewText.extend({
this.$el.find('.details__field-label').text(this.model.newField);
}
this.$el.addClass('details__field--can-edit-title');
if (this.isProtected === undefined) {
this.isProtected = this.value instanceof kdbxweb.ProtectedValue;
}
this.protectBtn = $('<div/>').addClass('details__field-value-btn details__field-value-btn-protect')
.toggleClass('details__field-value-btn-protect--protected', this.isProtected)
.appendTo(this.valueEl)
.mousedown(this.protectBtnClick.bind(this));
},
endEdit: function(newVal, extra) {
@ -32,7 +41,17 @@ var FieldViewCustom = FieldViewText.extend({
this.$el.removeClass('details__field--can-edit-title');
}
extra = _.extend({}, extra, { newField: this.model.newField });
FieldViewText.prototype.endEdit.call(this, newVal, extra);
if (!this.editing) {
return;
}
delete this.input;
if (typeof newVal === 'string') {
newVal = $.trim(newVal);
if (this.isProtected) {
newVal = kdbxweb.ProtectedValue.fromString(newVal);
}
}
FieldView.prototype.endEdit.call(this, newVal, extra);
},
startEditTitle: function() {
@ -92,6 +111,11 @@ var FieldViewCustom = FieldViewText.extend({
},
fieldValueBlur: function(e) {
if (this.protectJustChanged) {
this.protectJustChanged = false;
e.target.focus();
return;
}
this.endEdit(e.target.value);
},
@ -120,6 +144,13 @@ var FieldViewCustom = FieldViewText.extend({
$(e.target).unbind('blur');
this.endEditTitle(e.target.value);
}
},
protectBtnClick: function(e) {
e.stopPropagation();
this.isProtected = !this.isProtected;
this.protectBtn.toggleClass('details__field-value-btn-protect--protected', this.isProtected);
this.protectJustChanged = true;
}
});

View File

@ -32,8 +32,8 @@ var FieldViewText = FieldView.extend({
if (this.model.multiline) {
this.setInputHeight();
}
if (this.value && this.value.byteLength) {
$('<div/>').addClass('details__field-value-gen-btn').appendTo(this.valueEl)
if (this.model.canGen) {
$('<div/>').addClass('details__field-value-btn details__field-value-btn-gen').appendTo(this.valueEl)
.click(this.showGeneratorClick.bind(this))
.mousedown(this.showGenerator.bind(this));
}

View File

@ -4,7 +4,11 @@ var FieldViewText = require('./field-view-text');
var FieldViewUrl = FieldViewText.extend({
renderValue: function(value) {
return value ? '<a href="' + _.escape(value) + '" target="_blank">' + _.escape(value) + '</a>' : '';
return value ? '<a href="' + _.escape(this.fixUrl(value)) + '" target="_blank">' + _.escape(value) + '</a>' : '';
},
fixUrl: function(url) {
return url.indexOf(':') < 0 ? 'http://' + url : url;
}
});

View File

@ -91,8 +91,10 @@ var FieldView = Backbone.View.extend({
this.editing = false;
var oldValText = this.value && this.value.getText ? this.value.getText() : this.value;
var newValText = newVal && newVal.getText ? newVal.getText() : newVal;
var textEqual = _.isEqual(newValText, oldValText);
var protectedEqual = (newVal && typeof newVal.getText) === (this.value && typeof this.value.getText);
var arg;
if (newVal !== undefined && !_.isEqual(newValText, oldValText)) {
if (newVal !== undefined && (!textEqual || !protectedEqual)) {
arg = { val: newVal, field: this.model.name };
if (extra) {
_.extend(arg, extra);

View File

@ -22,6 +22,8 @@ var GeneratorView = Backbone.View.extend({
'click .gen__btn-ok': 'btnOkClick'
},
valuesMap: [3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,22,24,26,28,30,32,48,64],
initialize: function () {
$('body').one('click', this.remove.bind(this));
this.gen = _.clone(DefaultGenOpts);
@ -42,7 +44,7 @@ var GeneratorView = Backbone.View.extend({
},
lengthChange: function(e) {
var val = +e.target.value;
var val = this.valuesMap[e.target.value];
if (val !== this.gen.length) {
this.gen.length = val;
this.$el.find('.gen__length-range-val').html(val);
@ -61,6 +63,8 @@ var GeneratorView = Backbone.View.extend({
generate: function() {
this.password = PasswordGenerator.generate(this.gen);
this.resultEl.text(this.password);
var isLong = this.password.length > 32;
this.resultEl.toggleClass('gen__result--long-pass', isLong);
},
btnOkClick: function() {

View File

@ -0,0 +1,106 @@
'use strict';
var Backbone = require('backbone'),
Scrollable = require('../mixins/scrollable'),
IconSelectView = require('./icon-select-view'),
baron = require('baron');
var GrpView = Backbone.View.extend({
template: require('templates/grp.html'),
events: {
'click .grp__icon': 'showIconsSelect',
'click .grp__buttons-trash': 'moveToTrash',
'click .grp__back-button': 'returnToApp',
'blur #grp__field-title': 'titleBlur'
},
initialize: function() {
this.views = {};
},
render: function() {
this.removeSubView();
if (this.model) {
this.$el.html(this.template({
title: this.model.get('title'),
icon: this.model.get('icon') || 'folder',
readonly: this.model.get('top')
}));
if (!this.model.get('title')) {
this.$el.find('#grp__field-title').focus();
}
}
this.scroll = baron({
root: this.$el.find('.details__body')[0],
scroller: this.$el.find('.scroller')[0],
bar: this.$el.find('.scroller__bar')[0],
$: Backbone.$
});
this.scroller = this.$el.find('.scroller');
this.scrollerBar = this.$el.find('.scroller__bar');
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
this.pageResized();
return this;
},
removeSubView: function() {
if (this.views.sub) {
this.views.sub.remove();
delete this.views.sub;
}
},
showGroup: function(group) {
this.model = group;
this.render();
},
titleBlur: function(e) {
var title = $.trim(e.target.value);
if (title) {
if (!this.model.get('top') && e.target.value !== this.model.get('title')) {
this.model.setName(e.target.value);
}
} else {
if (this.model.isJustCreated) {
this.model.removeWithoutHistory();
Backbone.trigger('edit-group');
} else {
this.render();
}
}
},
showIconsSelect: function() {
if (this.views.sub) {
this.removeSubView();
} else {
var subView = new IconSelectView({el: this.$el.find('.grp__icons'), model: {iconId: this.model.get('iconId')}});
this.listenTo(subView, 'select', this.iconSelected);
subView.render();
this.views.sub = subView;
}
this.pageResized();
},
iconSelected: function(iconId) {
if (iconId !== this.model.get('iconId')) {
this.model.setIcon(iconId);
}
this.render();
},
moveToTrash: function() {
this.model.moveToTrash();
Backbone.trigger('select-all');
},
returnToApp: function() {
Backbone.trigger('edit-group');
}
});
_.extend(GrpView.prototype, Scrollable);
module.exports = GrpView;

View File

@ -1,13 +1,13 @@
'use strict';
var Backbone = require('backbone'),
IconMap = require('../../const/icon-map');
IconMap = require('../const/icon-map');
var DetailsIconView = Backbone.View.extend({
template: require('templates/details/details-icon.html'),
var IconSelectView = Backbone.View.extend({
template: require('templates/icon-select.html'),
events: {
'click .details__icons-icon': 'iconClick'
'click .icon-select__icon': 'iconClick'
},
render: function() {
@ -26,4 +26,4 @@ var DetailsIconView = Backbone.View.extend({
}
});
module.exports = DetailsIconView;
module.exports = IconSelectView;

View File

@ -5,6 +5,8 @@ var Backbone = require('backbone'),
Scrollable = require('../mixins/scrollable'),
ListSearchView = require('./list-search-view'),
EntryPresenter = require('../presenters/entry-presenter'),
DragDropInfo = require('../comp/drag-drop-info'),
AppSettingsModel = require('../models/app-settings-model'),
baron = require('baron');
var ListView = Backbone.View.extend({
@ -13,7 +15,8 @@ var ListView = Backbone.View.extend({
emptyTemplate: require('templates/list-empty.html'),
events: {
'click .list__item': 'itemClick'
'click .list__item': 'itemClick',
'dragstart .list__item': 'itemDragStart'
},
views: null,
@ -31,8 +34,10 @@ var ListView = Backbone.View.extend({
this.listenTo(this.views.search, 'select-prev', this.selectPrev);
this.listenTo(this.views.search, 'select-next', this.selectNext);
this.listenTo(this.views.search, 'create-entry', this.createEntry);
this.listenTo(this.views.search, 'create-group', this.createGroup);
this.listenTo(this, 'show', this.viewShown);
this.listenTo(this, 'hide', this.viewHidden);
this.listenTo(this, 'view-resize', this.viewResized);
this.listenTo(Backbone, 'filter', this.filterChanged);
this.listenTo(Backbone, 'entry-updated', this.entryUpdated);
@ -65,6 +70,9 @@ var ListView = Backbone.View.extend({
} else {
this.itemsEl.html(this.emptyTemplate());
}
if (typeof AppSettingsModel.instance.get('listViewWidth') === 'number') {
this.$el.width(AppSettingsModel.instance.get('listViewWidth'));
}
this.pageResized();
return this;
},
@ -105,6 +113,11 @@ var ListView = Backbone.View.extend({
this.selectItem(newEntry);
},
createGroup: function() {
var newGroup = this.model.createNewGroup();
Backbone.trigger('edit-group', newGroup);
},
selectItem: function(item) {
this.items.setActive(item);
Backbone.trigger('select-entry', item);
@ -129,6 +142,10 @@ var ListView = Backbone.View.extend({
this.views.search.hide();
},
viewResized: _.throttle(function(size) {
AppSettingsModel.instance.set('listViewWidth', size);
}, 1000),
filterChanged: function(filter) {
this.items = filter.entries;
this.render();
@ -138,6 +155,14 @@ var ListView = Backbone.View.extend({
var scrollTop = this.itemsEl[0].scrollTop;
this.render();
this.itemsEl[0].scrollTop = scrollTop;
},
itemDragStart: function(e) {
e.stopPropagation();
var id = $(e.target).closest('.list__item').attr('id');
e.dataTransfer.setData('text/entry', id);
e.dataTransfer.effectAllowed = 'move';
DragDropInfo.dragObject = this.items.get(id);
}
});

View File

@ -3,7 +3,8 @@
var Backbone = require('backbone'),
KeyHandler = require('../../comp/key-handler'),
Keys = require('../../const/keys'),
Alerts = require('../../comp/alerts');
Alerts = require('../../comp/alerts'),
DragDropInfo = require('../../comp/drag-drop-info');
var MenuItemView = Backbone.View.extend({
template: require('templates/menu/menu-item.html'),
@ -13,7 +14,13 @@ var MenuItemView = Backbone.View.extend({
'mouseout': 'mouseout',
'click .menu__item-option': 'selectOption',
'click': 'selectItem',
'dblclick': 'expandItem'
'dblclick': 'expandItem',
'click .menu__item-edit': 'editItem',
'click .menu__item-empty-trash': 'emptyTrash',
'dragstart': 'dragstart',
'dragover': 'dragover',
'dragleave': 'dragleave',
'drop' : 'drop'
},
iconEl: null,
@ -22,9 +29,12 @@ var MenuItemView = Backbone.View.extend({
initialize: function () {
this.itemViews = [];
this.listenTo(this.model, 'change:title', this.changeTitle);
this.listenTo(this.model, 'change:icon', this.changeIcon);
this.listenTo(this.model, 'change:active', this.changeActive);
this.listenTo(this.model, 'change:expanded', this.changeExpanded);
this.listenTo(this.model, 'change:cls', this.changeCls);
this.listenTo(this.model, 'delete', this.remove);
this.listenTo(this.model, 'insert', this.insertItem);
var shortcut = this.model.get('shortcut');
if (shortcut) {
KeyHandler.onKey(shortcut, this.selectItem, this, KeyHandler.SHORTCUT_OPT);
@ -42,13 +52,15 @@ var MenuItemView = Backbone.View.extend({
if (items && this.model.get('expanded')) {
items.forEach(function (item) {
if (item.get('visible')) {
var itemView = new MenuItemView({el: this.$el, model: item});
itemView.listenTo(itemView, 'select', this.itemSelected);
itemView.render();
this.itemViews.push(itemView);
this.insertItem(item);
}
}, this);
}
return this;
},
insertItem: function(item) {
this.itemViews.push(new MenuItemView({el: this.$el, model: item}).render());
},
remove : function() {
@ -69,7 +81,11 @@ var MenuItemView = Backbone.View.extend({
},
changeTitle: function(model, title) {
this.$el.find('.menu__item-title').text(title);
this.$el.find('.menu__item-title').first().text(title || '(no title)');
},
changeIcon: function(model, icon) {
this.iconEl[0].className = 'menu__item-icon fa ' + (icon ? 'fa-' + icon : 'menu__item-icon--no-icon');
},
changeActive: function(model, active) {
@ -89,8 +105,10 @@ var MenuItemView = Backbone.View.extend({
},
mouseover: function(e) {
this.$el.addClass('menu__item--hover');
e.stopPropagation();
if (!e.button) {
this.$el.addClass('menu__item--hover');
e.stopPropagation();
}
},
mouseout: function(e) {
@ -129,6 +147,62 @@ var MenuItemView = Backbone.View.extend({
this.model.toggleExpanded();
}
e.stopPropagation();
},
editItem: function(e) {
if (this.model.get('active') && this.model.get('editable')) {
e.stopPropagation();
Backbone.trigger('edit-group', this.model);
}
},
emptyTrash: function(e) {
e.stopPropagation();
Alerts.yesno({
header: 'Empty trash?',
body: 'You will not be able to put items back',
icon: 'minus-circle',
success: function() {
Backbone.trigger('empty-trash');
}
});
},
dropAllowed: function(e) {
return ['text/group', 'text/entry'].indexOf(e.dataTransfer.types[0]) >= 0;
},
dragstart: function(e) {
e.stopPropagation();
if (this.model.get('drag')) {
e.dataTransfer.setData('text/group', this.model.id);
e.dataTransfer.effectAllowed = 'move';
DragDropInfo.dragObject = this.model;
}
},
dragover: function(e) {
e.stopPropagation();
if (this.model.get('drop') && this.dropAllowed(e)) {
e.preventDefault();
this.$el.addClass('menu__item--drag');
}
},
dragleave: function(e) {
e.stopPropagation();
if (this.model.get('drop') && this.dropAllowed(e)) {
this.$el.removeClass('menu__item--drag');
}
},
drop: function(e) {
e.stopPropagation();
if (this.model.get('drop') && this.dropAllowed(e)) {
this.$el.removeClass('menu__item--drag');
this.model.moveHere(DragDropInfo.dragObject);
Backbone.trigger('refresh');
}
}
});

View File

@ -3,7 +3,8 @@
var Backbone = require('backbone'),
Resizable = require('../../mixins/resizable'),
MenuSectionView = require('./menu-section-view'),
DragView = require('../drag-view');
DragView = require('../drag-view'),
AppSettingsModel = require('../../models/app-settings-model');
var MenuView = Backbone.View.extend({
template: require('templates/menu/menu.html'),
@ -17,6 +18,7 @@ var MenuView = Backbone.View.extend({
initialize: function () {
this.listenTo(this.model, 'change:sections', this.menuChanged);
this.listenTo(this, 'view-resize', this.viewResized);
},
remove: function() {
@ -40,6 +42,9 @@ var MenuView = Backbone.View.extend({
}
this.sectionViews.push(sectionView);
}, this);
if (typeof AppSettingsModel.instance.get('menuViewWidth') === 'number') {
this.$el.width(AppSettingsModel.instance.get('menuViewWidth'));
}
return this;
},
@ -47,6 +52,10 @@ var MenuView = Backbone.View.extend({
this.render();
},
viewResized: _.throttle(function(size) {
AppSettingsModel.instance.set('menuViewWidth', size);
}, 1000),
switchVisibility: function(visible) {
this.$el.toggleClass('menu-visible', visible);
}

View File

@ -1,253 +0,0 @@
'use strict';
var Backbone = require('backbone'),
Keys = require('../const/keys'),
Alerts = require('../comp/alerts'),
SecureInput = require('../comp/secure-input'),
Launcher = require('../comp/launcher'),
DropboxLink = require('../comp/dropbox-link');
var OpenFileView = Backbone.View.extend({
template: require('templates/open-file.html'),
events: {
'click .open__file-btn-new': 'createNew',
'click .open__file-link-open': 'openFile',
'click .open__file-link-new': 'createNew',
'click .open__file-link-dropbox': 'openFromDropbox',
'click .open__file-link-demo': 'createDemo',
'click .open__file-link-name': 'resetFile',
'click .open__file-btn-key': 'openKeyFile',
'click .open__file-input[readonly]': 'openFile',
'change .open__file-ctrl': 'fileSelected',
'input .open__file-input': 'inputInput',
'keydown .open__file-input': 'inputKeydown',
'keypress .open__file-input': 'inputKeypress'
},
fileData: null,
keyFileData: null,
passwordInput: null,
dropboxLoading: null,
initialize: function () {
this.fileData = null;
this.keyFileData = null;
this.passwordInput = new SecureInput();
this.listenTo(this.model, 'change', this.render);
},
render: function () {
this.renderTemplate($.extend({
supportsDropbox: !Launcher,
dropboxLoading: this.dropboxLoading
}, this.model.attributes));
this.inputEl = this.$el.find('.open__file-input');
this.passwordInput.setElement(this.inputEl);
if (this.inputEl.attr('autofocus')) {
this.inputEl.focus();
}
if (this.model.get('error')) {
this.inputEl[0].selectionStart = 0;
this.inputEl[0].selectionEnd = this.inputEl.val().length;
}
return this;
},
openFile: function() {
if (!this.model.get('opening')) {
this.openAny('fileData', '.kdbx');
}
},
openKeyFile: function(e) {
if (e.target.hasAttribute('disabled')) {
return;
}
if (!this.model.get('opening')) {
if (this.keyFileData) {
this.keyFileData = null;
this.model.set('keyFileName', '');
} else {
this.openAny('keyFileData', '.key');
}
}
},
openAny: function(reading, ext) {
this.reading = reading;
this[reading] = null;
this.$el.find('.open__file-ctrl').attr('accept', ext).click();
},
resetFile: function() {
if (!this.model.get('opening')) {
this.passwordInput.reset();
this.fileData = null;
this.keyfileData = null;
this.model.clear({silent: true}).set(this.model.defaults);
}
},
fileSelected: function(e) {
this.inputEl.off('focus', this.inputFocus);
var file = e.target.files[0];
if (file) {
this.displayFile(file);
}
},
displayFile: function(file, complete) {
var reader = new FileReader();
reader.onload = (function(e) {
this[this.reading] = e.target.result;
if (this.reading === 'fileData') {
this.model.set('name', file.name.replace(/\.\w+$/i, ''));
if (file.path) {
this.model.set({ path: file.path, storage: file.storage || 'file' });
}
} else {
this.model.set('keyFileName', file.name);
}
if (complete) {
complete(true);
}
}).bind(this);
reader.onerror = (function() {
Alerts.error({ header: 'Failed to read file' });
this.showReadyToOpen();
if (complete) {
complete(false);
}
}).bind(this);
reader.readAsArrayBuffer(file);
},
setFile: function(file, keyFile) {
this.reading = 'fileData';
this.displayFile(file, (function(success) {
if (success && keyFile) {
this.reading = 'keyFileData';
this.displayFile(keyFile);
}
}).bind(this));
},
inputKeydown: function(e) {
var code = e.keyCode || e.which;
if (code === Keys.DOM_VK_RETURN && this.passwordInput.length) {
this.openDb();
} else if (code === Keys.DOM_VK_CAPS_LOCK) {
this.$el.find('.open__file-warning').addClass('hide');
}
},
inputKeypress: function(e) {
var charCode = e.keyCode || e.which;
var ch = String.fromCharCode(charCode),
lower = ch.toLowerCase(),
upper = ch.toUpperCase();
if (lower !== upper && !e.shiftKey) {
this.toggleCapsLockWarning(ch !== lower);
}
},
toggleCapsLockWarning: function(on) {
this.$el.find('.open__file-warning').toggleClass('hide', !on);
},
openDb: function() {
var arg = {
password: this.passwordInput.value,
fileData: this.fileData,
keyFileData: this.keyFileData
};
this.model.set({ opening: true, error: false });
this.afterPaint(function() {
this.trigger('select', arg);
});
},
createNew: function() {
if (!this.model.get('opening')) {
this.trigger('create');
}
},
createDemo: function() {
if (!this.model.get('opening')) {
this.trigger('create-demo');
}
},
openFromDropbox: function() {
if (this.dropboxLoading) {
return;
}
DropboxLink.authenticate((function(err) {
if (err) {
return;
}
this.dropboxLoading = 'file list';
this.render();
DropboxLink.getFileList((function(err, files) {
this.dropboxLoading = null;
if (err) {
this.render();
return;
}
var buttons = [];
files.forEach(function(file) {
buttons.push({ result: file, title: file.replace(/\.kdbx/i, '') });
});
if (!buttons.length) {
this.dropboxLoading = null;
this.render();
Alerts.error({
header: 'Nothing found',
body: 'You have no files in your Dropbox which could be opened. Files are searched in your Dropbox app folder: Apps/KeeWeb'
});
return;
}
buttons.push({ result: '', title: 'Cancel' });
Alerts.alert({
header: 'Select a file',
body: 'Select a file from your Dropbox which you would like to open',
icon: 'dropbox',
buttons: buttons,
esc: '',
click: '',
success: this.openDropboxFile.bind(this),
cancel: this.cancelOpenDropboxFile.bind(this)
});
}).bind(this));
}).bind(this));
},
openDropboxFile: function(file) {
var fileName = file.replace(/\.kdbx/i, '');
this.dropboxLoading = fileName;
this.render();
DropboxLink.openFile(file, (function(err, data) {
this.dropboxLoading = null;
if (err || !data || !data.size) {
this.render();
Alerts.error({ header: 'Failed to read file', body: 'Error reading Dropbox file: \n' + err });
return;
}
Object.defineProperties(data, {
storage: { value: 'dropbox' },
path: { value: file },
name: { value: fileName }
});
this.setFile(data);
}).bind(this));
},
cancelOpenDropboxFile: function() {
this.dropboxLoading = null;
this.render();
}
});
module.exports = OpenFileView;

View File

@ -1,75 +1,435 @@
'use strict';
var Backbone = require('backbone'),
OpenFileView = require('./open-file-view'),
Keys = require('../const/keys'),
Alerts = require('../comp/alerts'),
SecureInput = require('../comp/secure-input'),
FileModel = require('../models/file-model'),
Launcher = require('../comp/launcher');
Launcher = require('../comp/launcher'),
LastOpenFiles = require('../comp/last-open-files'),
Storage = require('../comp/storage'),
DropboxLink = require('../comp/dropbox-link');
var OpenView = Backbone.View.extend({
template: require('templates/open.html'),
views: null,
file: null,
events: {
'change .open__file-ctrl': 'fileSelected',
'click .open__icon-open': 'openFile',
'click .open__icon-new': 'createNew',
'click .open__icon-dropbox': 'openFromDropbox',
'click .open__icon-demo': 'createDemo',
'click .open__pass-input[readonly]': 'openFile',
'input .open__pass-input': 'inputInput',
'keydown .open__pass-input': 'inputKeydown',
'keypress .open__pass-input': 'inputKeypress',
'click .open__pass-enter-btn': 'openDb',
'click .open__settings-key-file': 'openKeyFile',
'click .open__last-item': 'openLast',
'dragover': 'dragover',
'dragleave': 'dragleave',
'drop': 'drop'
},
fileData: null,
keyFileData: null,
passwordInput: null,
dropboxLoading: null,
initialize: function () {
this.file = new FileModel();
this.views = { openFile: new OpenFileView({ model: this.file }) };
this.listenTo(this.file, 'change:open', this.fileOpened);
this.listenTo(this.views.openFile, 'select', this.selectFile);
this.listenTo(this.views.openFile, 'create', this.createNewFile);
this.listenTo(this.views.openFile, 'create-demo', this.createDemoFile);
this.fileData = null;
this.keyFileData = null;
this.passwordInput = new SecureInput();
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);
}
this.setElement($(this.template(this.model)).appendTo(this.$el));
this.views.openFile.setElement(this.$el).render();
this.renderTemplate({ supportsDropbox: !Launcher, lastOpenFiles: this.getLastOpenFiles() });
this.inputEl = this.$el.find('.open__pass-input');
this.passwordInput.setElement(this.inputEl);
return this;
},
selectFile: function(e) {
this.file.open(e.password, e.fileData, e.keyFileData);
getLastOpenFiles: function() {
return LastOpenFiles.all().map(function(f) {
switch (f.storage) {
case 'dropbox':
f.icon = 'dropbox';
break;
default:
f.icon = 'file-text';
break;
}
return f;
});
},
createNewFile: function() {
var name;
for (var i = 0; ; i++) {
name = 'New' + (i || '');
if (!this.model.files.getByName(name)) {
break;
remove: function() {
this.passwordInput.reset();
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) {
this.processFile(file);
}
},
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.displayOpenFile();
} else {
this.file.set('keyFileName', file.name);
this.displayOpenKeyFile();
}
if (complete) {
complete(true);
}
}).bind(this);
reader.onerror = (function() {
Alerts.error({ header: 'Failed to read file' });
this.showReadyToOpen();
if (complete) {
complete(false);
}
}).bind(this);
reader.readAsArrayBuffer(file);
},
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.focus();
},
displayOpenKeyFile: function() {
this.$el.find('.open__settings-key-file-name').text(this.file.get('keyFileName'));
this.$el.addClass('open--key-file');
this.inputEl.focus();
},
setFile: function(file, keyFile) {
this.reading = 'fileData';
this.processFile(file, (function(success) {
if (success && keyFile) {
this.reading = 'keyFileData';
this.processFile(keyFile);
}
}).bind(this));
},
createDemo: function() {
if (!this.file.get('opening')) {
if (!this.model.files.getByName('Demo')) {
this.file.createDemo();
} else {
this.trigger('cancel');
}
}
this.file.create(name);
},
createDemoFile: function() {
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);
}
},
fileOpened: function(model, open) {
if (open) {
this.model.addFile(this.file);
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);
}
}
},
openFile: function() {
if (!this.file.get('opening')) {
this.openAny('fileData');
}
},
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', '');
this.$el.removeClass('open--key-file');
this.$el.find('.open__settings-key-file-name').text('key file');
} else {
this.openAny('keyFileData');
}
}
},
openKeyFileFromDropbox: function() {
if (!this.file.get('opening')) {
DropboxLink.chooseFile((function(err, res) {
if (err) {
return;
}
this.keyFileData = res.data;
this.file.set('keyFileName', res.name);
this.displayOpenKeyFile();
}).bind(this));
}
},
openAny: function(reading, ext) {
this.reading = reading;
this[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);
});
}
},
inputKeydown: function(e) {
var code = e.keyCode || e.which;
if (code === Keys.DOM_VK_RETURN && this.passwordInput.length) {
this.openDb();
} else if (code === Keys.DOM_VK_CAPS_LOCK) {
this.$el.find('.open__pass-warning').removeClass('invisible');
} else if (code === Keys.DOM_VK_A) {
e.stopImmediatePropagation();
}
},
inputKeypress: function(e) {
var charCode = e.keyCode || e.which;
var ch = String.fromCharCode(charCode),
lower = ch.toLowerCase(),
upper = ch.toUpperCase();
if (lower !== upper && !e.shiftKey) {
this.toggleCapsLockWarning(ch !== lower);
}
},
toggleCapsLockWarning: function(on) {
this.$el.find('.open__file-warning').toggleClass('invisible', on);
},
openFromDropbox: function() {
if (this.dropboxLoading || this.file.get('opening')) {
return;
}
var that = this;
DropboxLink.authenticate(function(err) {
if (err) {
return;
}
that.dropboxLoading = 'file list';
that.displayDropboxLoading();
DropboxLink.getFileList(function(err, files) {
that.dropboxLoading = null;
that.displayDropboxLoading();
if (err) {
return;
}
var buttons = [];
var allFileNames = {};
files.forEach(function(file) {
var fileName = file.replace(/\.kdbx/i, '');
buttons.push({ result: file, title: fileName });
allFileNames[fileName] = true;
});
if (!buttons.length) {
Alerts.error({
header: 'Nothing found',
body: 'You have no files in your Dropbox which could be opened. Files are searched in your Dropbox app folder: Apps/KeeWeb'
});
return;
}
buttons.push({ result: '', title: 'Cancel' });
Alerts.alert({
header: 'Select a file',
body: 'Select a file from your Dropbox which you would like to open',
icon: 'dropbox',
buttons: buttons,
esc: '',
click: '',
success: that.openDropboxFile.bind(that),
cancel: function() {
that.dropboxLoading = null;
that.displayDropboxLoading();
}
});
LastOpenFiles.all().forEach(function(lastOpenFile) {
if (lastOpenFile.storage === 'dropbox' && !allFileNames[lastOpenFile.name]) {
that.delLast(lastOpenFile.name);
}
});
});
});
},
openDropboxFile: function(file) {
var fileName = file.replace(/\.kdbx/i, '');
this.dropboxLoading = fileName;
this.displayDropboxLoading();
var lastOpen = LastOpenFiles.byName(fileName);
var errorAlertCallback = lastOpen && lastOpen.storage === 'dropbox' && lastOpen.availOffline ?
this.dropboxErrorCallback.bind(this, fileName) : null;
DropboxLink.openFile(file, (function(err, data) {
this.dropboxLoading = null;
this.displayDropboxLoading();
if (err || !data || !data.size) {
return;
}
Object.defineProperties(data, {
storage: { value: 'dropbox' },
path: { value: file },
name: { value: fileName }
});
this.setFile(data);
}).bind(this), errorAlertCallback);
},
dropboxErrorCallback: function(fileName, alertConfig) {
alertConfig.body += '<br/>You have offline version of this file cached. Would you like to open it?';
alertConfig.buttons = [
{result: 'offline', title: 'Open offline file'},
{result: 'yes', title: 'OK'}
];
alertConfig.success = (function(result) {
if (result === 'offline') {
this.openCache(fileName, 'dropbox');
}
}).bind(this);
Alerts.error(alertConfig);
},
displayDropboxLoading: function() {
this.$el.find('.open__icon-dropbox .open__icon-i').toggleClass('flip3d', !!this.dropboxLoading);
},
openLast: function(e) {
if (this.dropboxLoading || this.file.get('opening')) {
return;
}
var name = $(e.target).closest('.open__last-item').data('name');
if ($(e.target).is('.open__last-item-icon-del')) {
this.delLast(name);
return;
}
var lastOpenFile = LastOpenFiles.byName(name);
switch (lastOpenFile.storage) {
case 'dropbox':
return this.openDropboxFile(lastOpenFile.path);
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) {
clearTimeout(this.dragTimeout);
}
if (this.model && !this.$el.hasClass('open--drag')) {
if (!this.$el.hasClass('open--drag')) {
this.$el.addClass('open--drag');
}
},
@ -93,23 +453,7 @@ var OpenView = Backbone.View.extend({
var dataFile = _.find(files, function(file) { return file.name.split('.').pop().toLowerCase() === 'kdbx'; });
var keyFile = _.find(files, function(file) { return file.name.split('.').pop().toLowerCase() === 'key'; });
if (dataFile) {
this.views.openFile.setFile(dataFile, keyFile);
}
},
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.views.openFile.setFile(file);
} catch (e) {
}
this.setFile(dataFile, keyFile);
}
}
});

View File

@ -1,7 +1,6 @@
'use strict';
var Backbone = require('backbone'),
AppSettingsModel = require('../../models/app-settings-model'),
FeatureDetector = require('../../util/feature-detector'),
PasswordGenerator = require('../../util/password-generator'),
Alerts = require('../../comp/alerts'),
@ -18,6 +17,7 @@ var SettingsAboutView = Backbone.View.extend({
'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-close': 'closeFile',
'change #settings__file-key-file': 'keyFileChange',
'mousedown #settings__file-file-select-link': 'triggerSelectFile',
'change #settings__file-file-select': 'fileSelected',
@ -98,26 +98,28 @@ var SettingsAboutView = Backbone.View.extend({
if (!this.validate()) {
return;
}
var data = this.model.getData();
var fileName = this.model.get('name') + '.kdbx';
if (Launcher) {
if (this.model.get('path')) {
this.saveToFileWithPath(this.model.get('path'), data);
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 {
Launcher.getSaveFileName(fileName, (function (path) {
if (path) {
this.saveToFileWithPath(path, data);
}
}).bind(this));
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();
}
}
} else {
var blob = new Blob([data], {type: 'application/octet-stream'});
FileSaver.saveAs(blob, fileName);
this.passwordChanged = false;
if (this.model.get('storage') !== 'dropbox') {
this.model.saved();
}
}
});
},
saveToFileWithPath: function(path, data) {
@ -125,10 +127,8 @@ var SettingsAboutView = Backbone.View.extend({
Launcher.writeFile(path, data);
this.passwordChanged = false;
this.model.saved(path, 'file');
if (!AppSettingsModel.instance.get('lastOpenFile')) {
AppSettingsModel.instance.set('lastOpenFile', path);
}
} catch (e) {
console.error('Error saving file', path, e);
Alerts.error({
header: 'Save error',
body: 'Error saving to file ' + path + ': \n' + e
@ -140,9 +140,10 @@ var SettingsAboutView = Backbone.View.extend({
if (!this.validate()) {
return;
}
var data = this.model.getXml();
var blob = new Blob([data], {type: 'text/xml'});
FileSaver.saveAs(blob, this.model.get('name') + '.xml');
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() {
@ -155,37 +156,67 @@ var SettingsAboutView = Backbone.View.extend({
if (this.model.get('syncing') || !this.validate()) {
return;
}
var data = this.model.getData();
var fileName = this.model.get('name') + '.kdbx';
this.model.set('syncing', true);
this.render();
DropboxLink.saveFile(fileName, data, overwrite, (function(err) {
if (err) {
this.model.set('syncing', false);
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: this.saveToDropbox.bind(this, true),
cancel: (function() { this.$el.find('#settings__file-name').focus(); }).bind(this)
});
var that = this;
this.model.getData(function(data) {
var fileName = that.model.get('name') + '.kdbx';
that.model.set('syncing', true);
that.render();
DropboxLink.saveFile(fileName, data, overwrite, function (err) {
if (err) {
that.model.set('syncing', false);
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
});
}
} else {
Alerts.error({
header: 'Save error',
body: 'Error saving to Dropbox: \n' + err
});
that.passwordChanged = false;
that.model.saved(fileName, 'dropbox');
that.render();
}
} else {
this.passwordChanged = false;
this.model.saved(fileName, 'dropbox');
this.render();
}
}).bind(this));
});
});
},
closeFile: function() {
if (this.model.get('modified')) {
var that = this;
Alerts.yesno({
header: 'Unsaved changes',
body: 'There are unsaved changes in this file',
buttons: [
//{result: 'save', title: 'Save and close'},
{result: 'close', title: 'Close and lose changes', error: true},
{result: '', title: 'Don\t close'}
],
success: function(result) {
if (result === 'close') {
that.closeFileNoCheck();
}
}
});
} else {
this.closeFileNoCheck();
}
},
closeFileNoCheck: function() {
Backbone.trigger('close-file', this.model);
},
keyFileChange: function(e) {

View File

@ -12,8 +12,9 @@ var SettingsGeneralView = Backbone.View.extend({
template: require('templates/settings/settings-general.html'),
events: {
'change #settings__general-theme': 'changeTheme',
'change #settings__general-auto-update': 'changeAutoUpdate',
'change .settings__general-theme': 'changeTheme',
'change .settings__general-expand': 'changeExpandGroups',
'change .settings__general-auto-update': 'changeAutoUpdate',
'click .settings__general-dev-tools-link': 'openDevTools'
},
@ -31,6 +32,7 @@ var SettingsGeneralView = Backbone.View.extend({
this.renderTemplate({
themes: this.allThemes,
activeTheme: AppSettingsModel.instance.get('theme'),
expandGroups: AppSettingsModel.instance.get('expandGroups'),
devTools: Launcher && Launcher.devTools,
canAutoUpdate: !!Launcher,
autoUpdate: Updater.enabledAutoUpdate(),
@ -87,6 +89,12 @@ var SettingsGeneralView = Backbone.View.extend({
}
},
changeExpandGroups: function(e) {
var expand = e.target.checked;
AppSettingsModel.instance.set('expandGroups', expand);
Backbone.trigger('refresh');
},
openDevTools: function() {
if (Launcher) {
Launcher.openDevTools();

View File

@ -66,6 +66,17 @@
}
}
&__grp {
@include flex(1);
@include display(flex);
overflow: hidden;
padding: $base-spacing;
position: relative;
@include mobile {
padding: $base-padding;
}
}
&__footer {
@include flex(0 0 auto);
@include th { border-top: light-border(); }

View File

@ -224,7 +224,7 @@
width: 12em;
@include flex(0 0 auto);
}
&-gen-btn {
&-btn {
@include position(absolute, 0 0 null null);
@include th { color: muted-color(); }
cursor: pointer;
@ -236,9 +236,13 @@
@include fa-icon();
cursor: pointer;
padding: $base-padding;
content: $fa-var-bolt;
}
}
&-btn-gen:before { content: $fa-var-bolt; }
&-btn-protect {
&:before { content: $fa-var-unlock; }
&--protected:before { content: $fa-var-lock; }
}
}
}
@ -354,36 +358,21 @@
.details--drag & { display: none; }
&-trash {
&-trash, &-trash-del {
@include icon-btn($error:true);
@include align-self(flex-end);
margin-right: 10px;
}
&-trash-del {
@include th { color: muted-color(); }
}
button ~ button {
margin-left: $small-spacing;
}
}
&__icons {
@include display(flex);
@include align-items(flex-start);
@include flex-direction(row);
@include justify-content(flex-start);
@include flex-wrap(wrap);
@include user-select(none);
&-icon {
@include area-selectable(bottom);
width: 26px;
text-align: center;
font-size: 20px;
padding: 10px;
&.details__icons-icon--active {
@include area-selected(bottom);
}
}
}
&__history {
@include flex(1 0 auto);
@include display(flex);

View File

@ -2,7 +2,7 @@
position: absolute;
@include dropdown;
padding: $base-spacing;
width: 10em;
width: 11em;
&__length-range {
}
&__check {
@ -15,11 +15,14 @@
&__result {
@include user-select(text);
font-family: $monospace-font-family;
margin-top: $base-padding-v;
height: 4em;
margin-top: 6px;
height: 50px;
text-align: center;
white-space: pre;
word-wrap: break-word;
&--long-pass {
font-size: .75em;
}
}
&__btn-wrap {
text-align: center;

View File

@ -0,0 +1,47 @@
.grp {
@include flex(1);
@include display(flex);
@include align-items(stretch);
@include flex-direction(column);
@include justify-content(flex-start);
@include scrollbar-on-hover;
width: 100%;
user-select: none;
&__back-button {
cursor: pointer;
position: absolute;
top: 0;
right: $base-padding-h;
padding: $base-padding-v * 2 0 1px 0;
z-index: 1;
}
>.scroller {
@include flex(1);
@include display(flex);
@include align-items(stretch);
@include flex-direction(column);
@include justify-content(flex-start);
overflow-x: hidden;
padding-top: 3px;
}
&__icon {
display: block;
font-size: $large-header-font-size;
padding: $base-padding-px;
@include align-self(flex-start);
@include area-selectable();
}
&__buttons {
@include display(flex);
@include flex-direction(row);
margin-top: $base-padding-v;
&-trash {
@include icon-btn($error:true);
}
}
}

View File

@ -44,6 +44,7 @@
&__item {
text-overflow: ellipsis;
overflow: hidden;
position: relative;
@include display(flex);
@include align-items(stretch);
@include flex-direction(column);
@ -121,8 +122,34 @@
}
}
&-edit, &-empty-trash {
display: none;
opacity: 0;
position: absolute;
right: .8em;
top: .6em;
cursor: pointer;
transition: opacity $base-duration $base-timing, color $base-duration $base-timing;
@include th {
color: muted-color();
&:hover { color: medium-color(); }
}
.menu__item--active>.menu__item-body>& {
display: block;
}
.menu__item--active>.menu__item-body:hover>& {
opacity: .5;
}
}
.fa {
margin-right: $base-padding-h / 2;
}
&--drag {
>.menu__item-body {
@include th { color: action-color(); }
}
}
}
}

View File

@ -3,79 +3,154 @@
@include display(flex);
@include align-self(stretch);
@include display(flex);
@include align-items(stretch);
@include align-items(center);
@include flex-direction(column);
@include justify-content(center);
@include th { background: background-color(); }
&__file {
&__icons {
@include display(flex);
@include align-items(stretch);
@include flex-direction(row);
@include justify-content(center);
.open--drag & {
display: none;
.open--drag & { display: none; }
}
&__icon {
text-align: center;
cursor: pointer;
margin: 20px;
transition: color $base-duration $base-timing;
&:hover { @include th { color: medium-color(); } }
&-i { font-size: 4em; }
&-text {
@include th { color: muted-color(); }
.open__icon:hover>& { @include th { color: medium-color(); } }
}
&-icon-wrap {
width: 50px;
@include mobile {
padding: 0 4px;
}
&__pass {
&-area {
@include display(flex);
@include align-items(stretch);
@include flex-direction(column);
@include justify-content(flex-start);
position: relative;
.open--drag & { display: none; }
}
&-warn-wrap {
@include display(flex);
@include flex-direction(row);
@include justify-content(flex-end);
}
&-field-wrap {
@include display(flex);
@include flex-direction(row);
@include justify-content(flex-start);
@include align-items(stretch);
margin-bottom: $base-padding-v;
}
&-enter-btn, &-opening-icon {
padding: .6em $base-spacing;
position: absolute;
left: 100%;
@include mobile { display: none; }
@include th { color: muted-color(); }
>i { font-size: 3em; }
}
&-enter-btn {
.open--file & {
cursor: pointer;
&:hover {
@include th { color: medium-color(); }
}
}
.open--opening & { display: none; }
}
&-opening-icon {
display: none;
.open--opening & { display: block; }
}
}
input[type=password].open__pass-input {
font-size: 22px;
margin-bottom: 0;
&[readonly] { cursor: pointer; }
@include mobile {
width: calc(100vw - 20px);
}
}
&__settings {
@include display(flex);
@include flex-direction(row);
@include justify-content(space-between);
@include align-items(stretch);
height: 2em;
&-key-file {
.open--file:not(.open--opening) & { cursor: pointer; }
.open--key-file & { @include th { color: medium-color(); } }
&-dropbox {
visibility: hidden;
&:hover {
.open--file & { visibility: visible; }
}
}
&-dropbox {
.open--key-file, .open--opening & { display: none; }
}
&:hover .open__settings-key-file-dropbox {
.open--file & { visibility: visible; }
}
}
&-icon, &-btn-new, &-btn-key {
font-size: 30px;
margin: 20px 0 0 0;
padding: 10px 0;
width: 100%;
text-align: center;
}
&-icon {
@include th { color: muted-color(); }
border-bottom: 1px transparent;
&--opening {
&:before {
content: $fa-var-spinner;
&-key-file, &-label-offline, &-label-offline:before, input[type=checkbox] + label.open__settings-label-offline:before,
&-key-file-dropbox {
@include th {
color: muted-color();
}
&:hover {
.open--file:not(.open--opening) & {
@include th { color: medium-color(); }
}
}
}
&-btn-new {
@include area-selectable;
}
&-btn-key {
&[disabled] {
@include th { color: muted-color(); }
}
&:not([disabled]) {
&-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 {
@include display(flex);
@include flex-direction(column);
@include justify-content(flex-start);
@include align-items(stretch);
margin-top: $base-spacing;
&-item {
@include display(flex);
@include flex-direction(row);
@include justify-content(flex-start);
@include align-items(baseline);
.open:not(.open--opening) & {
@include area-selectable;
}
&.open__file-btn-key--active, &.open__file-btn-key--active:hover {
@include th { color: action-color(); }
.open__file--error & { @include th { color: error-color(); } }
}
}
&-title {
@include th { color: muted-color(); }
font-size: 12px;
cursor: pointer;
height: 20px;
display: inline-block;
}
&-warning {
@include position(absolute, 0 0 null null);
margin: 0;
padding: 0;
}
&-password {
position: relative;
input[type=password].open__file-input {
font-size: 22px;
padding-right: 34px;
margin-bottom: 0;
width: 100%;
&[readonly] {
padding: $base-padding-v 0;
&-icon { width: 2em; }
&-text { @include flex-grow(1); }
&-icon-del {
opacity: 0;
.open__last-item:hover & {
opacity: .3;
cursor: pointer;
margin-right: $base-padding-h;
}
&.input--error {
@include th { color: muted-color(); }
&:hover {
.open__last-item:hover & { opacity: 1; }
@include th { color: medium-color(); }
}
}
}

View File

@ -53,13 +53,6 @@
}
}
&__select, &__input, &__pre, &__file-master-pass-label {
width: 60%;
@include tablet {
width: calc(100% - 20px);
}
}
&__select, &__input {
height: 2em;
}

View File

@ -20,11 +20,12 @@
@include th {
border: 1px solid action-color();
background-color: action-color();
color: text-color();
color: text-contrast-color(action-color());
&.btn-error, &.btn-silent {
border-color: base-border-color();
background-color: transparent;
color: text-contrast-color(background-color());
}
&:hover {
@ -38,7 +39,6 @@
border-color: action-background-color-focus();
background-color: rgba(action-background-color-focus(), .1);
}
color: text-color();
}
&:active {

View File

@ -20,6 +20,7 @@ $violet: #d946db;
@function muted-color-border() { @return mix(medium-color(), background-color(), 15%); }
@function text-selection-bg-color() { @return rgba(action-color(), .3); }
@function text-selection-bg-color-error() { @return rgba(error-color(), .8); }
@function text-contrast-color($bg) { @if (lightness($bg) >= lightness(background-color())) { @return text-color(); } @else { @return background-color(); } }
// Borders, shadows

View File

@ -243,3 +243,12 @@ $thumb-size: 14px;
@include th { background: text-color(); }
}
}
.input-base {
width: 60%;
@include tablet {
width: calc(100% - 20px);
}
}

View File

@ -24,7 +24,7 @@ button.pika-next {
}
.pika-table th {
@include th { color: medium-color(); }; // TODO
@include th { color: medium-color(); };
}
.pika-button, button.pika-button {

View File

@ -13,4 +13,9 @@
&__text, &__title {
padding: 0 $base-padding-h;
}
}
&__lower-btns {
position: absolute;
bottom: calc(#{$base-spacing} + 1px);
left: $base-spacing;
}
}

View File

@ -0,0 +1,18 @@
&.icon-select {
@include display(flex);
@include align-items(flex-start);
@include flex-direction(row);
@include justify-content(flex-start);
@include flex-wrap(wrap);
@include user-select(none);
&__icon {
@include area-selectable(bottom);
width: 26px;
text-align: center;
font-size: 20px;
padding: 10px;
&.icon-select__icon--active {
@include area-selected(bottom);
}
}
}

View File

@ -16,12 +16,14 @@
@import "common/empty";
@import "common/fx";
@import "common/help-tip";
@import "common/icon-select";
@import "common/modal";
@import "common/scroll";
@import "areas/app";
@import "areas/details";
@import "areas/footer";
@import "areas/grp";
@import "areas/generator";
@import "areas/list";
@import "areas/menu";

View File

@ -5,6 +5,7 @@
<div class="app__list"></div>
<div class="app__list-drag"></div>
<div class="app__details"></div>
<div class="app__grp"></div>
</div>
<div class="app__footer"></div>
</div>

View File

@ -2,5 +2,5 @@
<div class="details__attachment-preview-data"></div>
<i class="fa details__attachment-preview-icon"></i>
<div class="details__attachment-preview-download-text">Shift-click attachment button to download
or <span class="details__attachment-preview-download-text-shortcut"></span>-Delete to remove</div>
</div>
or <span class="details__attachment-preview-download-text-shortcut"></span>Delete to remove</div>
</div>

View File

@ -0,0 +1,6 @@
<div class="empty-block muted-color">
<h1 class="empty-block__title">To restore this group, please drag it to any group outside trash</h1>
<div class="empty-block__lower-btns">
<i class="details__buttons-trash-del fa fa-minus-circle"></i>
</div>
</div>

View File

@ -1,5 +0,0 @@
<div class="details__icons">
<% icons.forEach(function(icon, ix) { %>
<i class="fa fa-<%= icon %> details__icons-icon <%= ix === sel ? 'details__icons-icon--active' : '' %>" data-val="<%= ix %>"></i>
<% }); %>
</div>

View File

@ -26,7 +26,8 @@
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
</div>
<div class="details__buttons">
<% if (!deleted) { %><i class="details__buttons-trash fa fa-trash-o"></i><% } %>
<% if (deleted) { %><i class="details__buttons-trash-del fa fa-minus-circle"></i>
<% } else { %><i class="details__buttons-trash fa fa-trash-o"></i><% } %>
<div class="details__attachments">
<% attachments.forEach(function(attachment, ix) { %>
<div class="details__attachment" data-id="<%= ix %>"><i class="fa fa-<%= attachment.icon %>"></i> <%- attachment.title %></div>

View File

@ -1,6 +1,6 @@
<div class="gen">
<div>Length: <span class="gen__length-range-val"><%= opt.length %></span></div>
<input type="range" class="gen__length-range" value="16" min="2" max="32" />
<input type="range" class="gen__length-range" value="13" min="0" max="25" />
<div>
<div class="gen__check"><input type="checkbox" id="gen__check-upper"
data-id="upper" <%= opt.upper ? 'checked' : '' %>><label for="gen__check-upper">ABC</label></div>

22
app/templates/grp.html Normal file
View File

@ -0,0 +1,22 @@
<div class="grp">
<div class="grp__back-button">
return to app <i class="fa fa-external-link-square"></i>
</div>
<div class="scroller">
<h1>Group</h1>
<div class="grp__field">
<label for="grp__field-title">Name:</label>
<input type="text" class="input-base" id="grp__field-title" value="<%- title %>" size="50" maxlength="1024"
required <%= readonly ? 'readonly' : '' %> />
</div>
<label>Icon:</label>
<i class="fa fa-<%- icon %> grp__icon"></i>
<div class="grp__icons"></div>
</div>
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
<% if (!readonly) { %>
<div class="grp__buttons">
<i class="grp__buttons-trash fa fa-trash-o"></i>
</div>
<% } %>
</div>

View File

@ -0,0 +1,5 @@
<div class="icon-select">
<% icons.forEach(function(icon, ix) { %>
<i class="fa fa-<%= icon %> icon-select__icon <%= ix === sel ? 'icon-select__icon--active' : '' %>" data-val="<%= ix %>"></i>
<% }); %>
</div>

View File

@ -1,4 +1,4 @@
<div class="list__item <%= active ? 'list__item--active' : '' %> <%= expired ? 'list__item--expired' : '' %>" id="<%= id %>">
<div class="list__item <%= active ? 'list__item--active' : '' %> <%= expired ? 'list__item--expired' : '' %>" id="<%= id %>" draggable="true">
<i class="fa fa-<%= icon %> <%= color ? color+'-color' : '' %>"></i> <span
class="list__item-title"><%- title || '(no title)' %></span><span class="list__item-descr thin"><%- description %></span>
</div>
</div>

View File

@ -3,9 +3,9 @@
disabled ? 'menu__item--disabled' : '' %> <%=
options && options.length ? 'menu__item--with-options' : '' %> <%=
cls ? cls : '' %>">
<div class="menu__item-body">
<div class="menu__item-body" <%= drag ? 'draggable="true"' : '' %>>
<i class="menu__item-icon fa <%= icon ? 'fa-' + icon : 'menu__item-icon--no-icon' %>"></i><span
class="menu__item-title"><%- title %></span>
class="menu__item-title"><%- title || '(no title)' %></span>
<% if (options) { %>
<div class="menu__item-options">
<% options.forEach(function(option) { %>
@ -14,5 +14,11 @@
<% }); %>
</div>
<% } %>
<% if (typeof editable !== 'undefined') { %>
<i class="menu__item-edit fa fa-cog"></i>
<% } %>
<% if (filterKey === 'trash') { %>
<i class="menu__item-empty-trash fa fa-minus-circle"></i>
<% } %>
</div>
</div>

View File

@ -1,32 +0,0 @@
<div class="open__file <%= error ? 'open__file--error' : '' %>">
<input type="file" class="open__file-ctrl hide" />
<div class="open__file-icon-wrap open__file-icon-wrap--left">
<% if (name) { %>
<i class="open__file-icon fa fa-lock <%= opening ? 'open__file-icon--opening' : '' %>" disabled></i>
<% } else { %>
<i class="open__file-btn-new fa fa-plus" <%= opening ? 'disabled' : '' %>></i>
<% } %>
</div>
<div class="open__file-password">
<div class="open__file-title">
<% if (name) { %>
<a class="open__file-link-name muted-color" <%= opening ? 'disabled' : '' %>>Open another</a>
<% } else { %>
<a class="open__file-link-open muted-color" <%= opening ? 'disabled' : '' %>>Open</a> / <a
class="open__file-link-new muted-color" <%= opening ? 'disabled' : '' %>>New</a> / <a
class="open__file-link-demo muted-color" <%= opening ? 'disabled' : '' %>>Demo</a><% if (supportsDropbox) { %> / <a
class="open__file-link-dropbox muted-color" <%= (opening || dropboxLoading) ? 'disabled' : '' %>
>Dropbox<%= dropboxLoading ? ' (loading ' + dropboxLoading + '...)' : '' %></a><% } %>
<% } %>
</div>
<div class="open__file-warning muted-color hide"><i class="fa fa-exclamation-triangle"></i> Caps Lock is on</div>
<input class="open__file-input <%= error ? 'input--error' : '' %>" type="password"
size="30" autocomplete="off" maxlength="128"
placeholder="<%- name ? 'Password for ' + name : 'Click to open a file' %>"
<%= name ? 'autofocus' : 'readonly' %> <%= opening ? 'disabled' : '' %> />
</div>
<div class="open__file-icon-wrap open__file-icon-wrap--right">
<i class="open__file-btn-key fa fa-key <%= (keyFileName && !opening) ? 'open__file-btn-key--active' : '' %>"
<%= (name && !opening) ? '' : 'disabled' %>></i>
</div>
</div>

View File

@ -1,6 +1,61 @@
<div class="open">
<input type="file" class="open__file-ctrl hide-by-pos" />
<div class="open__icons">
<div class="open__icon open__icon-open">
<i class="fa fa-lock open__icon-i"></i>
<div class="open__icon-text">Open</div>
</div>
<div class="open__icon open__icon-new">
<i class="fa fa-plus open__icon-i"></i>
<div class="open__icon-text">New</div>
</div>
<% if (supportsDropbox) { %>
<div class="open__icon open__icon-dropbox">
<i class="fa fa-dropbox open__icon-i"></i>
<div class="open__icon-text">Dropbox</div>
</div>
<% } %>
<div class="open__icon open__icon-demo">
<i class="fa fa-magic open__icon-i"></i>
<div class="open__icon-text">Demo</div>
</div>
</div>
<div class="open__pass-area">
<div class="open__pass-warn-wrap">
<div class="open__pass-warning muted-color invisible"><i class="fa fa-exclamation-triangle"></i> Caps Lock is on</div>
</div>
<div class="open__pass-field-wrap">
<input class="open__pass-input" type="password" size="30" autocomplete="off" maxlength="128"
placeholder="Click to open a file" readonly />
<div class="open__pass-enter-btn"><i class="fa fa-level-down fa-rotate-90"></i></div>
<div class="open__pass-opening-icon"><i class="fa fa-spinner fa-spin"></i></div>
</div>
<div class="open__settings">
<div class="open__settings-key-file hide">
<i class="fa fa-key open__settings-key-file-icon"></i>
<span class="open__settings-key-file-name">key file</span>
<% if (supportsDropbox) { %>
<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 %>">
<i class="fa fa-<%= file.icon %> open__last-item-icon"></i>
<span class="open__last-item-text"><%- file.name %></span>
<i class="fa fa-times open__last-item-icon-del"></i>
</div>
<% }); %>
</div>
</div>
<div class="open__dropzone">
<i class="fa fa-lock muted-color open__dropzone-icon"></i>
<h1 class="muted-color open__dropzone-header">drop files here</h1>
</div>
</div>
</div>

View File

@ -21,37 +21,38 @@
<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>
<h2>Settings</h2>
<label for="settings__file-master-pass" class="settings__file-master-pass-label">Master password:
<label for="settings__file-master-pass" class="settings__file-master-pass-label input-base">Master password:
<span class="settings__file-master-pass-warning">
<i class="fa fa-warning"></i> password was changed; leave the field blank to use old password
</span>
</label>
<input type="password" class="settings__input" id="settings__file-master-pass" value="<%= password %>" />
<input type="password" class="settings__input input-base" id="settings__file-master-pass" value="<%= password %>" />
<label for="settings__file-key-file">Key file:</label>
<select class="settings__select settings__select-no-margin" id="settings__file-key-file"></select>
<select class="settings__select settings__select-no-margin input-base" id="settings__file-key-file"></select>
<a id="settings__file-file-select-link">Select a key file</a>
<input type="file" accept=".key" id="settings__file-file-select" class="hide" />
<h2>Names</h2>
<label for="settings__file-name">Name:</label>
<input type="text" class="settings__input" id="settings__file-name" value="<%- name %>" required />
<input type="text" class="settings__input input-base" id="settings__file-name" value="<%- name %>" required />
<label for="settings__file-def-user">Default username:</label>
<input type="text" class="settings__input" id="settings__file-def-user" value="<%- defaultUser %>" />
<input type="text" class="settings__input input-base" id="settings__file-def-user" value="<%- defaultUser %>" />
<h2>History</h2>
<div>
<input type="checkbox" class="settings__input" id="settings__file-trash" <%- recycleBinEnabled ? 'checked' : '' %> />
<input type="checkbox" class="settings__input input-base" id="settings__file-trash" <%- recycleBinEnabled ? 'checked' : '' %> />
<label for="settings__file-trash">Enable trash</label>
</div>
<label for="settings__file-hist-len">History length, keep last records per entry:</label>
<input type="text" pattern="\d+" required class="settings__input" id="settings__file-hist-len" value="<%= historyMaxItems %>" />
<input type="text" pattern="\d+" required class="settings__input input-base" id="settings__file-hist-len" value="<%= historyMaxItems %>" />
<label for="settings__file-hist-size">History size, total MB per file:</label>
<input type="text" pattern="\d+" required class="settings__input" id="settings__file-hist-size" value="<%= historyMaxSize %>" />
<input type="text" pattern="\d+" required class="settings__input input-base" id="settings__file-hist-size" value="<%= historyMaxSize %>" />
<h2>Advanced</h2>
<label for="settings__file-key-rounds">Key encryption rounds:</label>
<input type="text" pattern="\d+" required class="settings__input" id="settings__file-key-rounds" value="<%= keyEncryptionRounds %>" />
<input type="text" pattern="\d+" required class="settings__input input-base" id="settings__file-key-rounds" value="<%= keyEncryptionRounds %>" />
</div>

View File

@ -3,16 +3,20 @@
<h2>Appearance</h2>
<div>
<label for="settings__general-theme">Theme:</label>
<select class="settings__select" id="settings__general-theme">
<select class="settings__general-theme settings__select input-base" id="settings__general-theme">
<% _.forEach(themes, function(name, key) { %>
<option value="<%= key %>" <%= key === activeTheme ? 'selected' : '' %>><%- name %></option>
<% }); %>
</select>
</div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-expand" id="settings__general-expand" <%- expandGroups ? 'checked' : '' %> />
<label for="settings__general-expand">Show entries from all subgroups</label>
</div>
<% if (canAutoUpdate) { %>
<h2>Function</h2>
<div>
<input type="checkbox" class="settings__input" id="settings__general-auto-update" <%- autoUpdate ? 'checked' : '' %> />
<input type="checkbox" class="settings__input settings__general-auto-update" id="settings__general-auto-update" <%- autoUpdate ? 'checked' : '' %> />
<label for="settings__general-auto-update">Automatic updates</label>
<div><%- updateInfo %></div>
</div>

View File

@ -10,7 +10,7 @@
or <a href="http://antelle.net/" target="_blank">contact a developer</a> directly.
</p>
<p>App information:</p>
<pre class="settings__pre"><%= appInfo %></pre>
<pre class="settings__pre input-base"><%= appInfo %></pre>
<h2>Other platforms</h2>
<ul>
<li>
@ -28,4 +28,6 @@
<a href="<%= webAppLink %>" target="_blank">Web app</a>
</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>
</div>

View File

@ -26,10 +26,10 @@
"backbone": "~1.2.3",
"baron": "~0.7.11",
"bourbon": "~4.2.5",
"dropbox": "antelle/dropbox-js#0.10.3",
"dropbox": "antelle/dropbox-js#0.10.4",
"font-awesome": "~4.4.0",
"install": "~1.0.4",
"kdbxweb": "~0.1.10",
"kdbxweb": "~0.2.5",
"normalize.css": "~3.0.3",
"pikaday": "~1.3.3",
"zepto": "~1.1.6",

View File

@ -6,7 +6,8 @@
var app = require('app'),
BrowserWindow = require('browser-window'),
path = require('path'),
fs = require('fs');
fs = require('fs'),
Menu = require('menu');
var mainWindow = null,
openFile = process.argv.filter(function(arg) { return /\.kdbx$/i.test(arg); })[0],
@ -22,7 +23,7 @@ app.on('ready', function() {
width: 1000, height: 700, 'min-width': 600, 'min-height': 300,
icon: path.join(__dirname, 'icon.png')
});
mainWindow.setMenu(null);
setMenu();
if (fs.existsSync(htmlPath)) {
mainWindow.loadUrl('file://' + htmlPath);
} else {
@ -43,6 +44,42 @@ app.on('open-file', function(e, path) {
notifyOpenFile();
});
function setMenu() {
if (process.platform === 'darwin') {
var name = require('app').getName();
var template = [
{
label: name,
submenu: [
{ label: 'About ' + name, role: 'about' },
{ type: 'separator' },
{ label: 'Services', role: 'services', submenu: [] },
{ type: 'separator' },
{ label: 'Hide ' + name, accelerator: 'Command+H', role: 'hide' },
{ label: 'Hide Others', accelerator: 'Command+Shift+H', role: 'hideothers' },
{ label: 'Show All', role: 'unhide' },
{ type: 'separator' },
{ label: 'Quit', accelerator: 'Command+Q', click: function() { app.quit(); } }
]
},
{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' }
]
}
];
var menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
}
function notifyOpenFile() {
if (ready && openFile && mainWindow) {
openFile = openFile.replace(/"/g, '\\"').replace(/\\/g, '\\\\');

View File

@ -1,5 +1,5 @@
{
"name" : "KeeWeb",
"version" : "0.1.0",
"main" : "main.js"
"name": "KeeWeb",
"version": "0.2.1",
"main": "main.js"
}

42
features.md Normal file
View File

@ -0,0 +1,42 @@
# KeeWeb Features
## Available as a [webapp](https://antelle.github.io/keeweb/) or [desktop apps](https://github.com/antelle/keeweb/releases/latest)
![ ](https://habrastorage.org/files/a95/8ae/a5c/a958aea5c66d41a685ff283bfb453c39.png)
## Themes
![ ](https://habrastorage.org/files/b6e/ea1/425/b6eea14254c94449be2b3f335bd7ea63.png)
## Color favorites with quick access
![ ](https://habrastorage.org/files/dfd/619/89d/dfd61989d83542caba59f42d7e19714d.png)&nbsp;
![ ](https://habrastorage.org/files/074/f95/5ed/074f955edb334740900383dedc0c64b2.png)
## Multiple files support
![ ](https://habrastorage.org/files/16b/e21/5de/16be215de3314340a7c873e34f2e4adc.png)
## One search for all files
![ ](https://habrastorage.org/files/d9e/685/cd4/d9e685cd4177469da6123e60d7490486.png)&nbsp;
![ ](https://habrastorage.org/files/8fe/c69/7d3/8fec697d3392429a9e8d60f5b795abe5.png)
## And one trash
![ ](https://habrastorage.org/files/a3b/0c1/ca3/a3b0c1ca3d104b21af6372f915b318ef.png)
## Dropbox sync
![ ](https://habrastorage.org/files/8db/1d4/11c/8db1d411ca404fada936b470cfb9ca20.png)
## Easy tags input
![ ](https://habrastorage.org/files/4ba/000/bba/4ba000bbaba842b296749a6872b9e0aa.png)
## Drag&amp;drop
![ ](https://habrastorage.org/files/4c5/37a/e5f/4c537ae5f5404b64ad2c3110f7badc0b.png)
## Protected fields
![ ](https://habrastorage.org/files/40a/77b/fd2/40a77bfd2b66437fb8cf7ca007f7b45b.png)
## Password generator
![ ](https://habrastorage.org/files/69b/cbd/343/69bcbd3432ca456396c6d2a1ebc760df.png)&nbsp;
![ ](https://habrastorage.org/files/5bd/9f1/26f/5bd9f126f6d4496083258fc31d04db19.png)
## Offline
![ ](https://habrastorage.org/files/3d5/5d1/7c4/3d55d17c486b46529c38bdfff3284aa2.png)
## Shortcuts
![ ](https://habrastorage.org/files/f2c/115/19f/f2c11519f4e447a3bc18b03ec0437e34.png)
## History
![ ](https://habrastorage.org/files/980/92f/214/98092f2148754d91ac7711e57bc71747.png)
## Mobile
![ ](https://habrastorage.org/files/1f2/91b/b75/1f291bb75ffd4d35838be7f2d3f41345.png)
## Entry icons
![ ](https://habrastorage.org/files/a70/0ea/a55/a700eaa558b941acacbad19d6ad9dbc8.png)
## Inline image viewer
![ ](https://habrastorage.org/files/f80/e43/45d/f80e4345df354b4aa4ec67c29efb5153.png)
## Open-source
![ ](https://habrastorage.org/files/0dc/19f/38d/0dc19f38dcac4415a831cd0fec4ceef7.png)
[https://antelle.github.io/keeweb/](https://antelle.github.io/keeweb/)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View File

@ -0,0 +1,16 @@
'use strict';
var builder = require('electron-builder').init();
module.exports = function (grunt) {
grunt.registerMultiTask('electron_builder', 'Create app installer with electron-builder', function () {
var done = this.async();
builder.build(this.options(), function(err) {
if (err) {
grunt.warn(err);
return;
}
done();
});
});
};

View File

@ -1,23 +1,27 @@
{
"name": "keeweb",
"version": "0.0.6",
"version": "0.2.1",
"description": "KeePass web app",
"main": "Gulpfile.js",
"main": "Gruntfile.js",
"repository": "https://github.com/antelle/keeweb",
"dependencies": {},
"devDependencies": {
"base64-loader": "^1.0.0",
"cssnano": "^2.6.1",
"ejs-loader": "^0.2.1",
"electron-builder": "^2.1.1",
"exports-loader": "^0.6.2",
"grunt": "^0.4.5",
"grunt-bower-install-simple": "^1.1.4",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-compress": "^0.14.0",
"grunt-contrib-copy": "^0.8.1",
"grunt-contrib-htmlmin": "^0.4.0",
"grunt-contrib-jshint": "^0.11.2",
"grunt-contrib-uglify": "^0.9.1",
"grunt-contrib-watch": "^0.6.1",
"grunt-electron": "^2.0.1",
"grunt-gitinfo": "^0.1.7",
"grunt-inline-alt": "^0.3.10",
"grunt-postcss": "^0.6.0",
"grunt-sass": "^1.0.0",

33
release-notes.md Normal file
View File

@ -0,0 +1,33 @@
Release notes
-------------
##### vNext
`-` fixed protected field deletion
`+` remember menu and list width
##### v0.2.1 (2015-11-10)
Fixed KeePassX compatibility issues
##### v0.2.0 (2015-11-09)
UX improvements, offline, trash, protected fields, bugfixes
`+` improved open page ux
`+` trash management
`+` protected fields support
`+` keyfiles from Dropbox
`+` #17: option to hide entries from subgroups
`+` #5: groups and entries arranging
`+` #13: increase max generated password length
`+` #20: default http:// for urls without protocol
`-` #12: cannot edit entries without title
`-` #21: history inside history entries
##### v0.1.1 (2015-11-04)
Bugfix and performance enhancements
`+` support non-xml keyfiles
`+` removed limitation for extensions of opened files
`+` #10: using WebCrypto for better open performance
`-` #11: can create a group without name
`-` #3: desktop app quits without question about unsaved changes
`-` #2: shortcuts are not working in Mac app
##### v0.1.0 (2015-10-31)
First MVP

View File

@ -1,16 +0,0 @@
{
"osx" : {
"title": "KeeWeb",
"background": "../graphics/dmg-bg.png",
"icon": "../graphics/app.icns",
"icon-size": 80,
"contents": [
{ "x": 438, "y": 344, "type": "link", "path": "/Applications" },
{ "x": 192, "y": 344, "type": "file" }
]
},
"win" : {
"title": "KeeWeb",
"icon": "graphics/app.ico"
}
}

View File

@ -1,12 +0,0 @@
electron-packager electron/ KeeWeb --platform=darwin --arch=x64 --version=0.34.0 --icon=graphics/app.icns
electron-builder KeeWeb-darwin-x64/KeeWeb.app --platform=osx --out=. --config=./util/electron-builder.json
#electron-installer-dmg ./KeeWeb-darwin-x64/KeeWeb.app KeeWeb --out=./ --background=./graphics/dmg-bg.png --icon=./graphics/app.icns --overwrite
rm -rf ./KeeWeb-darwin-x64/
electron-packager electron/ KeeWeb --platform=linux --arch=x64 --version=0.34.0 --icon=graphics/app.ico
zip -r ./KeeWeb.linux.x64.zip ./KeeWeb-linux-x64/
rm -rf ./KeeWeb-linux-x64/
electron-packager electron/ KeeWeb --platform=win32 --arch=ia32 --version=0.34.0 --icon=graphics/app.ico
electron-builder KeeWeb-win32-ia32 --platform=win --out=. --config=./util/electron-builder.json
rm -rf ./KeeWeb-win32-ia32/

32
util/set-version.js Normal file
View File

@ -0,0 +1,32 @@
'use strict';
var fs = require('fs'),
path = require('path');
var version = process.argv[2];
if (!/\d+\.\d+\.\d+/.test(version)) {
console.error('Bad version. Usage: node set-version.js 1.2.3');
}
console.log('Change version to ' + version);
processFile('README.md', /\/download\/v[^\/]+/g);
processFile('package.json', /"version": "\d+\.\d+\.\d+"+/g);
processFile('electron/package.json', /"version": "\d+\.\d+\.\d+"+/g);
console.log('Done');
function processFile(name, regex) {
console.log('Replace: ' + name);
name = path.join(__dirname, '..', name);
var content = fs.readFileSync(name, 'utf8');
var replCount = 0;
content = content.replace(regex, function(match) {
replCount++;
return match.replace(/\d+\.\d+\.\d+/, version);
});
if (!replCount) {
throw 'No match found!';
}
fs.writeFileSync(name, content, 'utf8');
}