mirror of https://github.com/keeweb/keeweb.git
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:
commit
c7e7fe04ad
|
@ -19,5 +19,8 @@ trim_trailing_whitespace = false
|
|||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[.jshintrc]
|
||||
indent_size = 2
|
||||
|
||||
[*.scss]
|
||||
indent_size = 2
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
workspace.xml
|
||||
misc.xml
|
||||
.DS_Store
|
||||
node_modules/
|
||||
bower_components/
|
||||
|
|
|
@ -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>
|
|
@ -88,7 +88,9 @@
|
|||
"globals" : {
|
||||
"require": true,
|
||||
"module": true,
|
||||
"console": true,
|
||||
"performance": true,
|
||||
"$": true,
|
||||
"_": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
130
Gruntfile.js
130
Gruntfile.js
|
@ -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'
|
||||
]);
|
||||
};
|
||||
|
|
30
README.md
30
README.md
|
@ -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
45
TODO.md
|
@ -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
|
||||
|
|
BIN
app/favicon.png
BIN
app/favicon.png
Binary file not shown.
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.6 KiB |
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var DragDropInfo = {
|
||||
dragObject: null
|
||||
};
|
||||
|
||||
module.exports = DragDropInfo;
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
var Storage = {};
|
||||
|
||||
[
|
||||
require('./storage-cache'),
|
||||
require('./storage-dropbox'),
|
||||
require('./storage-file')
|
||||
].forEach(function(storage) {
|
||||
Storage[storage.name] = storage;
|
||||
});
|
||||
|
||||
module.exports = Storage;
|
|
@ -0,0 +1,114 @@
|
|||
'use strict';
|
||||
|
||||
var idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
|
||||
|
||||
var StorageCache = {
|
||||
name: 'cache',
|
||||
enabled: !!idb,
|
||||
|
||||
db: null,
|
||||
errorOpening: null,
|
||||
|
||||
init: function(callback) {
|
||||
if (this.db) {
|
||||
return callback();
|
||||
}
|
||||
var that = this;
|
||||
try {
|
||||
var req = idb.open('FilesCache');
|
||||
req.onerror = function (e) {
|
||||
console.error('Error opening indexed db', e);
|
||||
that.errorOpening = e;
|
||||
callback(e);
|
||||
};
|
||||
req.onsuccess = function (e) {
|
||||
that.db = e.target.result;
|
||||
callback();
|
||||
};
|
||||
req.onupgradeneeded = function (e) {
|
||||
var db = e.target.result;
|
||||
db.createObjectStore('files');
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error opening indexed db', e);
|
||||
callback(e);
|
||||
}
|
||||
},
|
||||
|
||||
save: function(id, data, callback) {
|
||||
this.init((function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
try {
|
||||
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
|
||||
req.onsuccess = function () {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
req.onerror = function () {
|
||||
console.error('Error saving to cache', id, req.error);
|
||||
if (callback) {
|
||||
callback(req.error);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error saving to cache', id, e);
|
||||
callback(e);
|
||||
}
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
load: function(id, callback) {
|
||||
this.init((function(err) {
|
||||
if (err) {
|
||||
return callback(null, err);
|
||||
}
|
||||
try {
|
||||
var req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
|
||||
req.onsuccess = function () {
|
||||
if (callback) {
|
||||
callback(req.result);
|
||||
}
|
||||
};
|
||||
req.onerror = function () {
|
||||
console.error('Error loading from cache', id, req.error);
|
||||
if (callback) {
|
||||
callback(null, req.error);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error loading from cache', id, e);
|
||||
callback(null, e);
|
||||
}
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
remove: function(id, callback) {
|
||||
this.init((function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
try {
|
||||
var req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
|
||||
req.onsuccess = function () {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
req.onerror = function () {
|
||||
console.error('Error removing from cache', id, req.error);
|
||||
if (callback) {
|
||||
callback(req.error);
|
||||
}
|
||||
};
|
||||
} catch(e) {
|
||||
console.error('Error removing from cache', id, e);
|
||||
callback(e);
|
||||
}
|
||||
}).bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = StorageCache;
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../launcher');
|
||||
|
||||
var StorageDropbox = {
|
||||
name: 'dropbox',
|
||||
enabled: !Launcher
|
||||
// TODO: move Dropbox storage operations here
|
||||
};
|
||||
|
||||
module.exports = StorageDropbox;
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../launcher');
|
||||
|
||||
var StorageFile = {
|
||||
name: 'file',
|
||||
enabled: !!Launcher
|
||||
// TODO: move file storage operations here
|
||||
};
|
||||
|
||||
module.exports = StorageFile;
|
|
@ -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 {
|
||||
|
|
|
@ -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' });
|
||||
},
|
||||
|
||||
|
|
|
@ -22,6 +22,10 @@ _.extend(Backbone.View.prototype, {
|
|||
return this;
|
||||
},
|
||||
|
||||
isHidden: function() {
|
||||
return this._hidden;
|
||||
},
|
||||
|
||||
afterPaint: function(callback) {
|
||||
this.requestAnimationFrame(function() {
|
||||
this.requestAnimationFrame(callback);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ var MenuItemModel = Backbone.Model.extend({
|
|||
cls: null,
|
||||
disabled: false,
|
||||
visible: true,
|
||||
drag: false,
|
||||
drop: false,
|
||||
filterKey: null,
|
||||
filterValue: null
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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)';
|
||||
|
|
|
@ -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;
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,13 +53,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__select, &__input, &__pre, &__file-master-pass-label {
|
||||
width: 60%;
|
||||
@include tablet {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
&__select, &__input {
|
||||
height: 2em;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -243,3 +243,12 @@ $thumb-size: 14px;
|
|||
@include th { background: text-color(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.input-base {
|
||||
width: 60%;
|
||||
@include tablet {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -13,4 +13,9 @@
|
|||
&__text, &__title {
|
||||
padding: 0 $base-padding-h;
|
||||
}
|
||||
}
|
||||
&__lower-btns {
|
||||
position: absolute;
|
||||
bottom: calc(#{$base-spacing} + 1px);
|
||||
left: $base-spacing;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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, '\\\\');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name" : "KeeWeb",
|
||||
"version" : "0.1.0",
|
||||
"main" : "main.js"
|
||||
"name": "KeeWeb",
|
||||
"version": "0.2.1",
|
||||
"main": "main.js"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
![ ](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)
|
||||
![ ](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&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)
|
||||
![ ](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.
BIN
graphics/app.ico
BIN
graphics/app.ico
Binary file not shown.
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 361 KiB |
|
@ -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();
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/
|
|
@ -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');
|
||||
}
|
Loading…
Reference in New Issue