mirror of https://github.com/keeweb/keeweb.git
Merge branch 'develop'
This commit is contained in:
commit
f315792efc
14
Gruntfile.js
14
Gruntfile.js
|
@ -16,7 +16,8 @@ module.exports = function(grunt) {
|
|||
var webpack = require('webpack');
|
||||
var pkg = require('./package.json');
|
||||
var dt = new Date().toISOString().replace(/T.*/, '');
|
||||
var electronVersion = '0.36.0';
|
||||
var electronVersion = '0.36.4';
|
||||
var appUpdateMinVersion = '0.5.0';
|
||||
|
||||
function replaceFont(css) {
|
||||
css.walkAtRules('font-face', function (rule) {
|
||||
|
@ -152,7 +153,8 @@ module.exports = function(grunt) {
|
|||
manifest: {
|
||||
options: {
|
||||
replacements: [
|
||||
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version }
|
||||
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version },
|
||||
{ pattern: '# updmin:v0.0.0', replacement: '# updmin:v' + appUpdateMinVersion }
|
||||
]
|
||||
},
|
||||
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
|
||||
|
@ -191,6 +193,7 @@ module.exports = function(grunt) {
|
|||
_: 'underscore/underscore-min.js',
|
||||
zepto: 'zepto/zepto.min.js',
|
||||
jquery: 'zepto/zepto.min.js',
|
||||
hbs: 'handlebars/runtime.js',
|
||||
kdbxweb: 'kdbxweb/dist/kdbxweb.js',
|
||||
dropbox: 'dropbox/lib/dropbox.min.js',
|
||||
baron: 'baron/baron.min.js',
|
||||
|
@ -201,7 +204,7 @@ module.exports = function(grunt) {
|
|||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.html$/, loader: StringReplacePlugin.replace('ejs', { replacements: [{
|
||||
{ test: /\.hbs$/, loader: StringReplacePlugin.replace('handlebars-loader', { replacements: [{
|
||||
pattern: /\r?\n\s*/g,
|
||||
replacement: function() { return '\n'; }
|
||||
}]})},
|
||||
|
@ -212,7 +215,8 @@ module.exports = function(grunt) {
|
|||
]})},
|
||||
{ test: /zepto(\.min)?\.js$/, loader: 'exports?Zepto; delete window.$; delete window.Zepto;' },
|
||||
{ test: /baron(\.min)?\.js$/, loader: 'exports?baron; delete window.baron;' },
|
||||
{ test: /pikadat\.js$/, loader: 'uglify' }
|
||||
{ test: /pikadat\.js$/, loader: 'uglify' },
|
||||
{ test: /handlebars/, loader: 'strip-sourcemap-loader' }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
|
@ -254,7 +258,7 @@ module.exports = function(grunt) {
|
|||
debounceDelay: 500
|
||||
},
|
||||
scripts: {
|
||||
files: ['app/scripts/**/*.js', 'app/templates/**/*.html'],
|
||||
files: ['app/scripts/**/*.js', 'app/templates/**/*.hbs'],
|
||||
tasks: ['webpack']
|
||||
},
|
||||
styles: {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
CACHE MANIFEST
|
||||
|
||||
# YYYY-MM-DD:v0.0.0
|
||||
# updmin:v0.0.0
|
||||
|
||||
CACHE:
|
||||
index.html
|
||||
|
|
|
@ -7,15 +7,16 @@ var AppModel = require('./models/app-model'),
|
|||
Alerts = require('./comp/alerts'),
|
||||
DropboxLink = require('./comp/dropbox-link'),
|
||||
Updater = require('./comp/updater'),
|
||||
ThemeChanger = require('./util/theme-changer');
|
||||
ThemeChanger = require('./util/theme-changer'),
|
||||
Locale = require('./util/locale');
|
||||
|
||||
$(function() {
|
||||
require('./mixins/view');
|
||||
|
||||
if (location.href.indexOf('state=') >= 0) {
|
||||
DropboxLink.receive();
|
||||
return;
|
||||
}
|
||||
require('./mixins/view');
|
||||
require('./helpers');
|
||||
KeyHandler.init();
|
||||
IdleTracker.init();
|
||||
|
||||
|
@ -24,14 +25,10 @@ $(function() {
|
|||
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. ' +
|
||||
'We strongly advice you to stop, unless you clearly understand what you\'re doing.' +
|
||||
'<br/><br/>' +
|
||||
'Yes, your database is encrypted but no one can guarantee that the app has not been modified on the way to you.',
|
||||
Alerts.error({ header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
buttons: [
|
||||
{ result: '', title: 'I understand the risks, continue', error: true }
|
||||
{ result: '', title: Locale.appSecWarnBtn, error: true }
|
||||
],
|
||||
complete: showApp
|
||||
});
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
var ModalView = require('../views/modal-view');
|
||||
var ModalView = require('../views/modal-view'),
|
||||
Locale = require('../util/locale');
|
||||
|
||||
var Alerts = {
|
||||
alertDisplayed: false,
|
||||
|
||||
buttons: {
|
||||
ok: {result: 'yes', title: 'OK'},
|
||||
yes: {result: 'yes', title: 'Yes'},
|
||||
no: {result: '', title: 'No'}
|
||||
ok: {result: 'yes', title: Locale.alertOk},
|
||||
yes: {result: 'yes', title: Locale.alertYes},
|
||||
no: {result: '', title: Locale.alertNo}
|
||||
},
|
||||
|
||||
alert: function(config) {
|
||||
|
@ -31,7 +32,7 @@ var Alerts = {
|
|||
|
||||
notImplemented: function() {
|
||||
this.alert({
|
||||
header: 'Not Implemented',
|
||||
header: Locale.notImplemented,
|
||||
body: '',
|
||||
icon: 'exclamation-triangle',
|
||||
buttons: [this.buttons.ok],
|
||||
|
|
|
@ -51,6 +51,7 @@ var CopyPaste = {
|
|||
}).bind(null, Launcher.getClipboardText()), clipboardSeconds * 1000);
|
||||
}, 0);
|
||||
}
|
||||
return clipboardSeconds;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ var Dropbox = require('dropbox'),
|
|||
Alerts = require('./alerts'),
|
||||
Launcher = require('./launcher'),
|
||||
Logger = require('../util/logger'),
|
||||
Locale = require('../util/locale'),
|
||||
Links = require('../const/links');
|
||||
|
||||
var logger = new Logger('dropbox');
|
||||
|
@ -134,9 +135,9 @@ var DropboxLink = {
|
|||
if (!isValidKey()) {
|
||||
Alerts.error({
|
||||
icon: 'dropbox',
|
||||
header: 'Dropbox not configured',
|
||||
body: 'So, you are using KeeWeb on your own server? Good!<br/>' +
|
||||
'<a href="' + Links.SelfHostedDropbox + '" target="blank">Some configuration</a> is required to make Dropbox work, it\'s just 3 steps away.'
|
||||
header: Locale.dropboxNotConfigured,
|
||||
body: Locale.dropboxNotConfiguredBody1 + '<br/>' + Locale.dropboxNotConfiguredBody2.replace('{}',
|
||||
'<a href="' + Links.SelfHostedDropbox + '" target="blank">' + Locale.dropboxNotConfiguredLink + '</a>')
|
||||
});
|
||||
return complete(DropboxCustomErrors.BadKey);
|
||||
}
|
||||
|
@ -166,9 +167,9 @@ var DropboxLink = {
|
|||
if (!Alerts.alertDisplayed) {
|
||||
Alerts.yesno({
|
||||
icon: 'dropbox',
|
||||
header: 'Dropbox Login',
|
||||
body: 'To continue, you have to sign in to Dropbox.',
|
||||
buttons: [{result: 'yes', title: 'Sign In'}, {result: '', title: 'Cancel'}],
|
||||
header: Locale.dropboxLogin,
|
||||
body: Locale.dropboxLoginBody,
|
||||
buttons: [{result: 'yes', title: Locale.alertSignIn}, {result: '', title: Locale.alertCancel}],
|
||||
success: (function () {
|
||||
this.authenticate(function (err) { callback(!err); });
|
||||
}).bind(this),
|
||||
|
@ -181,42 +182,42 @@ var DropboxLink = {
|
|||
break;
|
||||
case Dropbox.ApiError.NOT_FOUND:
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'The file was not found. Has it been removed from another computer?'
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxNotFoundBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.OVER_QUOTA:
|
||||
alertCallback({
|
||||
header: 'Dropbox Full',
|
||||
body: 'Your Dropbox is full, there\'s no space left anymore.'
|
||||
header: Locale.dropboxFull,
|
||||
body: Locale.dropboxFullBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.RATE_LIMITED:
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'Too many requests to Dropbox have been made by this app. Please, try again later.'
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxRateLimitedBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.NETWORK_ERROR:
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Network Error',
|
||||
body: 'Network error occured during Dropbox sync. Please, check your connection and try again.'
|
||||
header: Locale.dropboxNetError,
|
||||
body: Locale.dropboxNetErrorBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.INVALID_PARAM:
|
||||
case Dropbox.ApiError.OAUTH_ERROR:
|
||||
case Dropbox.ApiError.INVALID_METHOD:
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ' + err.status
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxErrorBody + err.status
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.CONFLICT:
|
||||
break;
|
||||
default:
|
||||
alertCallback({
|
||||
header: 'Dropbox Sync Error',
|
||||
body: 'Something went wrong during Dropbox sync. Please, try again later. Error: ' + err
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxErrorRepeatBody + err
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone');
|
||||
var Backbone = require('backbone'),
|
||||
Locale = require('../util/locale');
|
||||
var Launcher;
|
||||
|
||||
if (window.process && window.process.versions && window.process.versions.electron) {
|
||||
|
@ -28,9 +29,9 @@ if (window.process && window.process.versions && window.process.versions.electro
|
|||
defaultPath = this.req('path').join(homePath, defaultPath);
|
||||
}
|
||||
this.remReq('dialog').showSaveDialog({
|
||||
title: 'Save Passwords Database',
|
||||
title: Locale.launcherSave,
|
||||
defaultPath: defaultPath,
|
||||
filters: [{ name: 'KeePass files', extensions: ['kdbx'] }]
|
||||
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
|
||||
}, cb);
|
||||
},
|
||||
getUserDataPath: function(fileName) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var kdbxweb = require('kdbxweb');
|
||||
|
||||
var SecureInput = function() {
|
||||
this.el = null;
|
||||
this.minChar = 0x1400 + Math.round(Math.random() * 100);
|
||||
|
@ -73,7 +75,22 @@ SecureInput.prototype._isSpecialChar = function(ch) {
|
|||
Object.defineProperty(SecureInput.prototype, 'value', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return { value: this.pseudoValue, salt: this.salt };
|
||||
var pseudoValue = this.pseudoValue,
|
||||
salt = this.salt,
|
||||
len = pseudoValue.length,
|
||||
byteLength = 0,
|
||||
valueBytes = new Uint8Array(len * 4),
|
||||
saltBytes = kdbxweb.Random.getBytes(len * 4),
|
||||
ch, bytes;
|
||||
for (var i = 0; i < len; i++) {
|
||||
ch = String.fromCharCode(pseudoValue.charCodeAt(i) ^ salt[i]);
|
||||
bytes = kdbxweb.ByteUtils.stringToBytes(ch);
|
||||
for (var j = 0; j < bytes.length; j++) {
|
||||
valueBytes[byteLength] = bytes[j] ^ saltBytes[byteLength];
|
||||
byteLength++;
|
||||
}
|
||||
}
|
||||
return new kdbxweb.ProtectedValue(valueBytes.buffer.slice(0, byteLength), saltBytes.buffer.slice(0, byteLength));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ var Updater = {
|
|||
that.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
var updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
|
||||
var prevLastVersion = UpdateModel.instance.get('lastVersion');
|
||||
UpdateModel.instance.set({
|
||||
status: 'ok',
|
||||
|
@ -105,7 +106,8 @@ var Updater = {
|
|||
lastSuccessCheckDate: dt,
|
||||
lastVersionReleaseDate: new Date(match[1]),
|
||||
lastVersion: match[2],
|
||||
lastcheckError: null
|
||||
lastCheckError: null,
|
||||
lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null
|
||||
});
|
||||
UpdateModel.instance.save();
|
||||
that.scheduleNextCheck();
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
var Timeouts = {
|
||||
AutoSync: 30 * 1000 * 60
|
||||
AutoSync: 30 * 1000 * 60,
|
||||
CopyTip: 1500,
|
||||
AutoHideHint: 3000
|
||||
};
|
||||
|
||||
module.exports = Timeouts;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
|
||||
var Handlebars = require('hbs');
|
||||
|
||||
Handlebars.registerHelper('cmp', function(lvalue, rvalue, op, options) {
|
||||
var cond;
|
||||
switch (op) {
|
||||
case '<':
|
||||
cond = lvalue < rvalue;
|
||||
break;
|
||||
case '>':
|
||||
cond = lvalue > rvalue;
|
||||
break;
|
||||
case '>=':
|
||||
cond = lvalue >= rvalue;
|
||||
break;
|
||||
case '<=':
|
||||
cond = lvalue <= rvalue;
|
||||
break;
|
||||
case '===':
|
||||
case '==':
|
||||
cond = lvalue === rvalue;
|
||||
break;
|
||||
case '!==':
|
||||
case '!=':
|
||||
cond = lvalue !== rvalue;
|
||||
break;
|
||||
}
|
||||
return cond ? options.fn(this) : options.inverse(this);
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Handlebars = require('hbs');
|
||||
|
||||
Handlebars.registerHelper('ifemptyoreq', function(lvalue, rvalue, options) {
|
||||
return !lvalue || lvalue === rvalue ? options.fn(this) : options.inverse(this);
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Handlebars = require('hbs');
|
||||
|
||||
Handlebars.registerHelper('ifeq', function(lvalue, rvalue, options) {
|
||||
return lvalue === rvalue ? options.fn(this) : options.inverse(this);
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Handlebars = require('hbs');
|
||||
|
||||
Handlebars.registerHelper('ifneq', function(lvalue, rvalue, options) {
|
||||
return lvalue !== rvalue ? options.fn(this) : options.inverse(this);
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
require('./cmp');
|
||||
require('./ifeq');
|
||||
require('./ifneq');
|
||||
require('./ifemptyoreq');
|
||||
require('./res');
|
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
var Handlebars = require('hbs'),
|
||||
Locale = require('../util/locale');
|
||||
|
||||
Handlebars.registerHelper('res', function(key, options) {
|
||||
var value = Locale[key];
|
||||
if (value) {
|
||||
var ix = value.indexOf('{}');
|
||||
if (ix >= 0) {
|
||||
value = value.replace('{}', options.fn(this));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('Res', function(key) {
|
||||
var value = Locale[key];
|
||||
if (value) {
|
||||
value = value[0].toUpperCase() + value.substr(1);
|
||||
}
|
||||
return value;
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
'use strict';
|
||||
|
||||
var kdbxweb = require('kdbxweb');
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.isProtected = true;
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) {
|
||||
var value = this._value, salt = this._salt;
|
||||
var b, b1, b2, b3;
|
||||
for (var i = 0, len = value.length; i < len; i++) {
|
||||
b = value[i] ^ salt[i];
|
||||
if (b < 128) {
|
||||
fn(b);
|
||||
continue;
|
||||
}
|
||||
i++; b1 = value[i] ^ salt[i];
|
||||
if (i === len) { break; }
|
||||
if (b >= 192 && b < 224) {
|
||||
fn(((b & 0x1f) << 6) | (b1 & 0x3f));
|
||||
continue;
|
||||
}
|
||||
i++; b2 = value[i] ^ salt[i];
|
||||
if (i === len) { break; }
|
||||
if (b >= 224 && b < 240) {
|
||||
fn(((b & 0xf) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f));
|
||||
}
|
||||
i++; b3 = value[i] ^ salt[i];
|
||||
if (i === len) { break; }
|
||||
if (b >= 240 && b < 248) {
|
||||
var c = ((b & 7) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
|
||||
if (c <= 0xffff) {
|
||||
fn(c);
|
||||
} else {
|
||||
c ^= 0x10000;
|
||||
fn(0xd800 | (c >> 10));
|
||||
fn(0xdc00 | (c & 0x3ff));
|
||||
}
|
||||
}
|
||||
// skip error
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(kdbxweb.ProtectedValue.prototype, 'textLength', {
|
||||
get: function() {
|
||||
var textLength = 0;
|
||||
this.forEachChar(function() { textLength++; });
|
||||
return textLength;
|
||||
}
|
||||
});
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.includesLower = function(findLower) {
|
||||
var matches = false;
|
||||
var foundSeqs = [];
|
||||
var ix = 0;
|
||||
var len = findLower.length;
|
||||
this.forEachChar(function(ch) {
|
||||
ch = String.fromCharCode(ch).toLowerCase();
|
||||
if (matches) {
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < foundSeqs.length; i++) {
|
||||
var seqIx = ++foundSeqs[i];
|
||||
if (findLower[seqIx] !== ch) {
|
||||
foundSeqs.splice(i, 1);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (seqIx === len - 1) {
|
||||
matches = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (findLower[0] === ch) {
|
||||
foundSeqs.push(0);
|
||||
}
|
||||
ix++;
|
||||
});
|
||||
return matches;
|
||||
};
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.equals = function(other) {
|
||||
if (!other) {
|
||||
return false;
|
||||
}
|
||||
if (!other.isProtected) {
|
||||
return this.textLength === other.length && this.includes(other);
|
||||
}
|
||||
if (other === this) {
|
||||
return true;
|
||||
}
|
||||
var len = this.byteLength;
|
||||
if (len !== other.byteLength) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < len; i++) {
|
||||
if ((this._value[i] ^ this._salt[i]) !== (other._value[i] ^ other._salt[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = kdbxweb.ProtectedValue;
|
|
@ -1,8 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone');
|
||||
var Backbone = require('backbone'),
|
||||
FeatureDetector = require('../util/feature-detector'),
|
||||
baron = require('baron');
|
||||
|
||||
var isEnabled = FeatureDetector.isDesktop();
|
||||
|
||||
var Scrollable = {
|
||||
createScroll: function(opts) {
|
||||
opts.$ = Backbone.$;
|
||||
if (isEnabled) {
|
||||
this.scroll = baron(opts);
|
||||
}
|
||||
this.scroller = this.$el.find('.scroller');
|
||||
this.scrollerBar = this.$el.find('.scroller__bar');
|
||||
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
|
||||
},
|
||||
|
||||
pageResized: function() {
|
||||
// TODO: check size on window resize
|
||||
//if (this.checkSize && (!e || e.source === 'window')) {
|
||||
|
@ -20,7 +34,9 @@ var Scrollable = {
|
|||
},
|
||||
|
||||
initScroll: function() {
|
||||
this.listenTo(Backbone, 'page-geometry', this.pageResized);
|
||||
if (isEnabled) {
|
||||
this.listenTo(Backbone, 'page-geometry', this.pageResized);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone');
|
||||
var Backbone = require('backbone'),
|
||||
Tip = require('../util/tip');
|
||||
|
||||
_.extend(Backbone.View.prototype, {
|
||||
hide: function() {
|
||||
|
@ -32,6 +33,10 @@ _.extend(Backbone.View.prototype, {
|
|||
});
|
||||
},
|
||||
|
||||
setTimeout: function(callback) {
|
||||
setTimeout(callback.bind(this), 0);
|
||||
},
|
||||
|
||||
requestAnimationFrame: function(callback) {
|
||||
requestAnimationFrame(callback.bind(this));
|
||||
},
|
||||
|
@ -48,6 +53,7 @@ _.extend(Backbone.View.prototype, {
|
|||
this.$el.replaceWith(el);
|
||||
}
|
||||
this.setElement(el);
|
||||
Tip.createTips(el);
|
||||
},
|
||||
|
||||
_parentRemove: Backbone.View.prototype.remove,
|
||||
|
|
|
@ -14,6 +14,8 @@ var Backbone = require('backbone'),
|
|||
IdGenerator = require('../util/id-generator'),
|
||||
Logger = require('../util/logger');
|
||||
|
||||
require('../mixins/protected-value-ex');
|
||||
|
||||
var AppModel = Backbone.Model.extend({
|
||||
defaults: {},
|
||||
|
||||
|
@ -464,10 +466,16 @@ var AppModel = Backbone.Model.extend({
|
|||
Storage[storage].load(path, function(err, data, stat) {
|
||||
logger.info('Load from storage', stat, err);
|
||||
if (err) { return complete(err); }
|
||||
file.mergeOrUpdate(data, function(err) {
|
||||
file.mergeOrUpdate(data, options.remoteKey, function(err) {
|
||||
logger.info('Merge complete', err);
|
||||
that.refresh();
|
||||
if (err) { return complete(err); }
|
||||
if (err) {
|
||||
if (err.code === 'InvalidKey') {
|
||||
logger.info('Remote key changed, request to enter new key');
|
||||
Backbone.trigger('remote-key-changed', { file: file });
|
||||
}
|
||||
return complete(err);
|
||||
}
|
||||
if (stat && stat.rev) {
|
||||
logger.info('Update rev in file info');
|
||||
fileInfo.set('rev', stat.rev);
|
||||
|
|
|
@ -17,7 +17,8 @@ var AppSettingsModel = Backbone.Model.extend({
|
|||
minimizeOnClose: false,
|
||||
tableView: false,
|
||||
colorfulIcons: false,
|
||||
lockOnMinimize: true
|
||||
lockOnMinimize: true,
|
||||
helpTipCopyShown: false
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
@ -27,7 +28,6 @@ var AppSettingsModel = Backbone.Model.extend({
|
|||
load: function() {
|
||||
var data = SettingsStore.load('app-settings');
|
||||
if (data) {
|
||||
if (data.theme === 'd') { data.theme = 'db'; } // TODO: remove in v0.6
|
||||
this.set(data, {silent: true});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -9,8 +9,9 @@ var Backbone = require('backbone'),
|
|||
|
||||
var EntryModel = Backbone.Model.extend({
|
||||
defaults: {},
|
||||
urlRegex: /^https?:\/\//i,
|
||||
|
||||
buildInFields: ['Title', 'Password', 'Notes', 'URL', 'UserName'],
|
||||
builtInFields: ['Title', 'Password', 'Notes', 'URL', 'UserName'],
|
||||
|
||||
initialize: function() {
|
||||
},
|
||||
|
@ -33,6 +34,7 @@ var EntryModel = Backbone.Model.extend({
|
|||
this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
|
||||
this.notes = entry.fields.Notes || '';
|
||||
this.url = entry.fields.URL || '';
|
||||
this.displayUrl = this._getDisplayUrl(entry.fields.URL);
|
||||
this.user = entry.fields.UserName || '';
|
||||
this.iconId = entry.icon;
|
||||
this.icon = this._iconFromId(entry.icon);
|
||||
|
@ -97,18 +99,30 @@ var EntryModel = Backbone.Model.extend({
|
|||
return IconMap[id];
|
||||
},
|
||||
|
||||
_getDisplayUrl: function(url) {
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
return url.replace(this.urlRegex, '');
|
||||
},
|
||||
|
||||
_colorToModel: function(color) {
|
||||
return color ? Color.getNearest(color) : null;
|
||||
},
|
||||
|
||||
_fieldsToModel: function(fields) {
|
||||
return _.omit(fields, this.buildInFields);
|
||||
return _.omit(fields, this.builtInFields);
|
||||
},
|
||||
|
||||
_attachmentsToModel: function(binaries) {
|
||||
var att = [];
|
||||
_.forEach(binaries, function(data, title) {
|
||||
att.push(AttachmentModel.fromAttachment({ data: data, title: title }));
|
||||
if (data && data.ref) {
|
||||
data = this.file.db.meta.binaries[data.ref];
|
||||
}
|
||||
if (data) {
|
||||
att.push(AttachmentModel.fromAttachment({data: data, title: title}));
|
||||
}
|
||||
}, this);
|
||||
return att;
|
||||
},
|
||||
|
@ -128,10 +142,98 @@ var EntryModel = Backbone.Model.extend({
|
|||
matches: function(filter) {
|
||||
return !filter ||
|
||||
(!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
|
||||
(!filter.textLower || this.searchText.indexOf(filter.textLower) >= 0) &&
|
||||
(!filter.textLower || (filter.advanced ? this.matchesAdv(filter) : this.searchText.indexOf(filter.textLower) >= 0)) &&
|
||||
(!filter.color || filter.color === true && this.searchColor || this.searchColor === filter.color);
|
||||
},
|
||||
|
||||
matchesAdv: function(filter) {
|
||||
var adv = filter.advanced;
|
||||
var search, match;
|
||||
if (adv.regex) {
|
||||
try { search = new RegExp(filter.text, adv.cs ? '' : 'i'); }
|
||||
catch (e) { return false; }
|
||||
match = this.matchRegex;
|
||||
} else if (adv.cs) {
|
||||
search = filter.text;
|
||||
match = this.matchString;
|
||||
} else {
|
||||
search = filter.textLower;
|
||||
match = this.matchStringLower;
|
||||
}
|
||||
if (this.matchEntry(this.entry, adv, match, search)) {
|
||||
return true;
|
||||
}
|
||||
if (adv.history) {
|
||||
for (var i = 0, len = this.entry.history.length; i < len; i++) {
|
||||
if (this.matchEntry(this.entry.history[0], adv, match, search)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
matchString: function(str, find) {
|
||||
if (str.isProtected) {
|
||||
return str.includes(find);
|
||||
}
|
||||
return str.indexOf(find) >= 0;
|
||||
},
|
||||
|
||||
matchStringLower: function(str, findLower) {
|
||||
if (str.isProtected) {
|
||||
return str.includesLower(findLower);
|
||||
}
|
||||
return str.toLowerCase().indexOf(findLower) >= 0;
|
||||
},
|
||||
|
||||
matchRegex: function(str, regex) {
|
||||
if (str.isProtected) {
|
||||
str = str.getText();
|
||||
}
|
||||
return regex.test(str);
|
||||
},
|
||||
|
||||
matchEntry: function(entry, adv, compare, search) {
|
||||
var matchField = this.matchField;
|
||||
if (adv.user && matchField(entry, 'UserName', compare, search)) {
|
||||
return true;
|
||||
}
|
||||
if (adv.url && matchField(entry, 'URL', compare, search)) {
|
||||
return true;
|
||||
}
|
||||
if (adv.notes && matchField(entry, 'Notes', compare, search)) {
|
||||
return true;
|
||||
}
|
||||
if (adv.pass && matchField(entry, 'Password', compare, search)) {
|
||||
return true;
|
||||
}
|
||||
if (adv.other && matchField(entry, 'Title', compare, search)) {
|
||||
return true;
|
||||
}
|
||||
var matches = false;
|
||||
if (adv.other || adv.protect) {
|
||||
var builtInFields = this.builtInFields;
|
||||
var fieldNames = Object.keys(entry.fields);
|
||||
matches = fieldNames.some(function (field) {
|
||||
if (builtInFields.indexOf(field) >= 0) {
|
||||
return false;
|
||||
}
|
||||
if (typeof entry.fields[field] === 'string') {
|
||||
return adv.other && matchField(entry, field, compare, search);
|
||||
} else {
|
||||
return adv.protect && matchField(entry, field, compare, search);
|
||||
}
|
||||
});
|
||||
}
|
||||
return matches;
|
||||
},
|
||||
|
||||
matchField: function(entry, field, compare, search) {
|
||||
var val = entry.fields[field];
|
||||
return val ? compare(val, search) : false;
|
||||
},
|
||||
|
||||
setColor: function(color) {
|
||||
this._entryModified();
|
||||
this.entry.bgColor = Color.getKnownBgColor(color);
|
||||
|
@ -166,8 +268,8 @@ var EntryModel = Backbone.Model.extend({
|
|||
|
||||
setField: function(field, val) {
|
||||
this._entryModified();
|
||||
var hasValue = val && (typeof val === 'string' || val instanceof kdbxweb.ProtectedValue && val.byteLength);
|
||||
if (hasValue || this.buildInFields.indexOf(field) >= 0) {
|
||||
var hasValue = val && (typeof val === 'string' || val.isProtected && val.byteLength);
|
||||
if (hasValue || this.builtInFields.indexOf(field) >= 0) {
|
||||
this.entry.fields[field] = val;
|
||||
} else {
|
||||
delete this.entry.fields[field];
|
||||
|
@ -181,7 +283,15 @@ var EntryModel = Backbone.Model.extend({
|
|||
|
||||
addAttachment: function(name, data) {
|
||||
this._entryModified();
|
||||
this.entry.binaries[name] = kdbxweb.ProtectedValue.fromBinary(data);
|
||||
var binaryId;
|
||||
for (var i = 0; ; i++) {
|
||||
if (!this.file.db.meta.binaries[i]) {
|
||||
binaryId = i.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.file.db.meta.binaries[binaryId] = data;
|
||||
this.entry.binaries[name] = { ref: binaryId };
|
||||
this._fillByEntry();
|
||||
},
|
||||
|
||||
|
|
|
@ -44,20 +44,6 @@ var FileModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
open: function(password, fileData, keyFileData, callback) {
|
||||
var len = password.value.length,
|
||||
byteLength = 0,
|
||||
value = new Uint8Array(len * 4),
|
||||
salt = kdbxweb.Random.getBytes(len * 4),
|
||||
ch, bytes;
|
||||
for (var i = 0; i < len; i++) {
|
||||
ch = String.fromCharCode(password.value.charCodeAt(i) ^ password.salt[i]);
|
||||
bytes = kdbxweb.ByteUtils.stringToBytes(ch);
|
||||
for (var j = 0; j < bytes.length; j++) {
|
||||
value[byteLength] = bytes[j] ^ salt[byteLength];
|
||||
byteLength++;
|
||||
}
|
||||
}
|
||||
password = new kdbxweb.ProtectedValue(value.buffer.slice(0, byteLength), salt.buffer.slice(0, byteLength));
|
||||
try {
|
||||
var credentials = new kdbxweb.Credentials(password, keyFileData);
|
||||
var ts = logger.ts();
|
||||
|
@ -68,7 +54,7 @@ var FileModel = Backbone.Model.extend({
|
|||
} else {
|
||||
this.db = db;
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: len });
|
||||
this.setOpenFile({ passwordLength: password.textLength });
|
||||
if (keyFileData) {
|
||||
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
|
||||
}
|
||||
|
@ -162,13 +148,38 @@ var FileModel = Backbone.Model.extend({
|
|||
this.trigger('reload', this);
|
||||
},
|
||||
|
||||
mergeOrUpdate: function(fileData, callback) {
|
||||
kdbxweb.Kdbx.load(fileData, this.db.credentials, (function(remoteDb, err) {
|
||||
mergeOrUpdate: function(fileData, remoteKey, callback) {
|
||||
var credentials;
|
||||
if (remoteKey) {
|
||||
credentials = new kdbxweb.Credentials(kdbxweb.ProtectedValue.fromString(''));
|
||||
if (remoteKey.password) {
|
||||
credentials.setPassword(remoteKey.password);
|
||||
} else {
|
||||
credentials.passwordHash = this.db.credentials.passwordHash;
|
||||
}
|
||||
if (remoteKey.keyFileName) {
|
||||
if (remoteKey.keyFileData) {
|
||||
credentials.setKeyFile(remoteKey.keyFileData);
|
||||
} else {
|
||||
credentials.keyFileHash = this.db.credentials.keyFileHash;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
credentials = this.db.credentials;
|
||||
}
|
||||
kdbxweb.Kdbx.load(fileData, credentials, (function(remoteDb, err) {
|
||||
if (err) {
|
||||
logger.error('Error opening file to merge', err.code, err.message, err);
|
||||
} else {
|
||||
if (this.get('modified')) {
|
||||
try {
|
||||
if (remoteKey && remoteDb.meta.keyChanged > this.db.meta.keyChanged) {
|
||||
this.db.credentials = remoteDb.credentials;
|
||||
this.set('keyFileName', remoteKey.keyFileName || '');
|
||||
if (remoteKey.password) {
|
||||
this.set('passwordLength', remoteKey.password.textLength);
|
||||
}
|
||||
}
|
||||
this.db.merge(remoteDb);
|
||||
} catch (e) {
|
||||
logger.error('File merge error', e);
|
||||
|
@ -255,9 +266,11 @@ var FileModel = Backbone.Model.extend({
|
|||
getData: function(cb) {
|
||||
this.db.cleanup({
|
||||
historyRules: true,
|
||||
customIcons: true
|
||||
customIcons: true,
|
||||
binaries: true
|
||||
});
|
||||
var that = this;
|
||||
this.db.cleanup({ binaries: true });
|
||||
this.db.save(function(data, err) {
|
||||
if (err) {
|
||||
logger.error('Error saving file', that.get('name'), err);
|
||||
|
@ -298,7 +311,7 @@ var FileModel = Backbone.Model.extend({
|
|||
setPassword: function(password) {
|
||||
this.db.credentials.setPassword(password);
|
||||
this.db.meta.keyChanged = new Date();
|
||||
this.set({ passwordLength: password.byteLength, passwordChanged: true });
|
||||
this.set({ passwordLength: password.textLength, passwordChanged: true });
|
||||
this.setModified();
|
||||
},
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ var Backbone = require('backbone'),
|
|||
MenuSectionCollection = require('../../collections/menu/menu-section-collection'),
|
||||
MenuSectionModel = require('./menu-section-model'),
|
||||
GroupsMenuModel = require('./groups-menu-model'),
|
||||
Locale = require('../../util/locale'),
|
||||
Keys = require('../../const/keys'),
|
||||
Colors = require('../../const/colors');
|
||||
|
||||
|
@ -16,17 +17,18 @@ var MenuModel = Backbone.Model.extend({
|
|||
|
||||
initialize: function() {
|
||||
this.menus = {};
|
||||
this.allItemsSection = new MenuSectionModel([{ title: 'All Items', icon: 'th-large', active: true, shortcut: Keys.DOM_VK_A, filterKey: '*' }]);
|
||||
this.allItemsSection = new MenuSectionModel([{ title: Locale.menuAllItems, icon: 'th-large', active: true,
|
||||
shortcut: Keys.DOM_VK_A, filterKey: '*' }]);
|
||||
this.groupsSection = new GroupsMenuModel();
|
||||
this.colorsSection = new MenuSectionModel([{ title: 'Colors', icon: 'bookmark', shortcut: Keys.DOM_VK_C, cls: 'menu__item-colors',
|
||||
filterKey: 'color', filterValue: true }]);
|
||||
this.colorsSection = new MenuSectionModel([{ title: Locale.menuColors, icon: 'bookmark', shortcut: Keys.DOM_VK_C,
|
||||
cls: 'menu__item-colors', filterKey: 'color', filterValue: true }]);
|
||||
this.colorsItem = this.colorsSection.get('items').models[0];
|
||||
var defTags = [{ title: 'Tags', icon: 'tags', defaultItem: true,
|
||||
disabled: { header: 'No tags', body: 'You can add new tags while editing fields, in tags section.', icon: 'tags' } }];
|
||||
var defTags = [{ title: Locale.menuTags, icon: 'tags', defaultItem: true,
|
||||
disabled: { header: Locale.menuAlertNoTags, body: Locale.menuAlertNoTagsBody, icon: 'tags' } }];
|
||||
this.tagsSection = new MenuSectionModel(defTags);
|
||||
this.tagsSection.set({ scrollable: true, drag: true });
|
||||
this.tagsSection.defaultItems = defTags;
|
||||
this.trashSection = new MenuSectionModel([{ title: 'Trash', icon: 'trash', shortcut: Keys.DOM_VK_D,
|
||||
this.trashSection = new MenuSectionModel([{ title: Locale.menuTrash, icon: 'trash', shortcut: Keys.DOM_VK_D,
|
||||
filterKey: 'trash', filterValue: true, drop: true }]);
|
||||
Colors.AllColors.forEach(function(color) { this.colorsSection.get('items').models[0]
|
||||
.addOption({ cls: 'fa ' + color + '-color', value: color, filterValue: color }); }, this);
|
||||
|
@ -38,10 +40,10 @@ var MenuModel = Backbone.Model.extend({
|
|||
this.trashSection
|
||||
]);
|
||||
|
||||
this.generalSection = new MenuSectionModel([{ title: 'General', icon: 'cog', page: 'general', active: true }]);
|
||||
this.shortcutsSection = new MenuSectionModel([{ title: 'Shortcuts', icon: 'keyboard-o', page: 'shortcuts' }]);
|
||||
this.aboutSection = new MenuSectionModel([{ title: 'About', icon: 'info', page: 'about' }]);
|
||||
this.helpSection = new MenuSectionModel([{ title: 'Help', icon: 'question', page: 'help' }]);
|
||||
this.generalSection = new MenuSectionModel([{ title: Locale.menuSetGeneral, icon: 'cog', page: 'general', active: true }]);
|
||||
this.shortcutsSection = new MenuSectionModel([{ title: Locale.menuSetShortcuts, icon: 'keyboard-o', page: 'shortcuts' }]);
|
||||
this.aboutSection = new MenuSectionModel([{ title: Locale.menuSetAbout, icon: 'info', page: 'about' }]);
|
||||
this.helpSection = new MenuSectionModel([{ title: Locale.menuSetHelp, icon: 'question', page: 'help' }]);
|
||||
this.filesSection = new MenuSectionModel();
|
||||
this.filesSection.set({ scrollable: true, grow: true });
|
||||
this.menus.settings = new MenuSectionCollection([
|
||||
|
|
|
@ -10,6 +10,7 @@ var UpdateModel = Backbone.Model.extend({
|
|||
lastVersion: null,
|
||||
lastVersionReleaseDate: null,
|
||||
lastCheckError: null,
|
||||
lastCheckUpdMin: null,
|
||||
status: null,
|
||||
updateStatus: null,
|
||||
updateError: null,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Format = require('../util/format');
|
||||
var Format = require('../util/format'),
|
||||
Locale = require('../util/locale');
|
||||
|
||||
var EntryPresenter = function(descField, noColor, activeEntryId) {
|
||||
this.entry = null;
|
||||
|
@ -24,7 +25,7 @@ EntryPresenter.prototype = {
|
|||
get color() { return this.entry ? (this.entry.color || (this.entry.customIcon ? this.noColor : undefined)) : undefined; },
|
||||
get title() { return this.entry ? this.entry.title : this.group.get('title'); },
|
||||
get notes() { return this.entry ? this.entry.notes : undefined; },
|
||||
get url() { return this.entry ? this.entry.url : undefined; },
|
||||
get url() { return this.entry ? this.entry.displayUrl : undefined; },
|
||||
get user() { return this.entry ? this.entry.user : undefined; },
|
||||
get active() { return this.entry ? this.entry.id === this.activeEntryId : this.group.active; },
|
||||
get created() { return this.entry ? Format.dtStr(this.entry.created) : undefined; },
|
||||
|
@ -33,19 +34,19 @@ EntryPresenter.prototype = {
|
|||
get tags() { return this.entry ? this.entry.tags : false; },
|
||||
get description() {
|
||||
if (!this.entry) {
|
||||
return '[Group]';
|
||||
return '[' + Locale.listGroup + ']';
|
||||
}
|
||||
switch (this.descField) {
|
||||
case 'website':
|
||||
return this.url || '(no website)';
|
||||
return this.url || '(' + Locale.listNoWebsite + ')';
|
||||
case 'user':
|
||||
return this.user || '(no user)';
|
||||
return this.user || '(' + Locale.listNoUser + ')';
|
||||
case 'created':
|
||||
return this.created;
|
||||
case 'updated':
|
||||
return this.updated;
|
||||
case 'attachments':
|
||||
return this.entry.attachments.map(function(a) { return a.title; }).join(', ') || '(no attachments)';
|
||||
return this.entry.attachments.map(function(a) { return a.title; }).join(', ') || '(' + Locale.listNoAttachments + ')';
|
||||
default:
|
||||
return this.notes || this.url || this.user;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ var Format = {
|
|||
':' + this.pad(dt.getSeconds(), 2) : '';
|
||||
},
|
||||
dStr: function(dt) {
|
||||
return dt ? dt.getDate() + ' ' + Locale.MonthsShort[dt.getMonth()] + ' ' + dt.getFullYear() : '';
|
||||
return dt ? dt.getDate() + ' ' + Locale.monthsShort[dt.getMonth()] + ' ' + dt.getFullYear() : '';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,316 @@
|
|||
'use strict';
|
||||
|
||||
var Locale = {
|
||||
Months: ['January','February','March','April','May','June','July','August','September','October','November','December'],
|
||||
MonthsShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
|
||||
Weekdays: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
|
||||
WeekdaysShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
|
||||
months: ['January','February','March','April','May','June','July','August','September','October','November','December'],
|
||||
monthsShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
|
||||
weekdays: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
|
||||
weekdaysShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
|
||||
|
||||
retToApp: 'return to app',
|
||||
name: 'name',
|
||||
icon: 'icon',
|
||||
title: 'title',
|
||||
password: 'password',
|
||||
user: 'user',
|
||||
website: 'website',
|
||||
tags: 'tags',
|
||||
notes: 'notes',
|
||||
noTitle: 'no title',
|
||||
or: 'or',
|
||||
notImplemented: 'Not Implemented',
|
||||
|
||||
menuAllItems: 'All Items',
|
||||
menuColors: 'Colors',
|
||||
menuTags: 'Tags',
|
||||
menuTrash: 'Trash',
|
||||
menuSetGeneral: 'General',
|
||||
menuSetShortcuts: 'Shortcuts',
|
||||
menuSetHelp: 'Help',
|
||||
menuSetAbout: 'About',
|
||||
menuAlertNoTags: 'No tags',
|
||||
menuAlertNoTagsBody: 'You can add new tags while editing fields, in tags section.',
|
||||
menuEmptyTrash: 'Empty Trash',
|
||||
menuEmptyTrashAlert: 'Empty Trash?',
|
||||
menuEmptyTrashAlertBody: 'You will not be able to put items back',
|
||||
|
||||
alertYes: 'Yes',
|
||||
alertNo: 'No',
|
||||
alertOk: 'OK',
|
||||
alertCancel: 'Cancel',
|
||||
alertSignIn: 'Sign In',
|
||||
alertCopy: 'Copy',
|
||||
alertClose: 'Close',
|
||||
|
||||
footerOpen: 'Open / New',
|
||||
footerSyncError: 'Sync error',
|
||||
|
||||
genLen: 'Length',
|
||||
grpTitle: 'Group',
|
||||
grpSearch: 'Enable searching entries in this group',
|
||||
|
||||
keyChangeTitle: 'Master Key Changed',
|
||||
keyChangeMessage: 'Master key was changed for this database. Please enter a new key',
|
||||
|
||||
iconFavTitle: 'Download and use website favicon',
|
||||
iconSelCustom: 'Select custom icon',
|
||||
|
||||
listEmptyTitle: 'Empty',
|
||||
listEmptyAdd: 'add with {} button above',
|
||||
listGroup: 'Group',
|
||||
listNoWebsite: 'no website',
|
||||
listNoUser: 'no user',
|
||||
listNoAttachments: 'no attachments',
|
||||
|
||||
searchAddNew: 'Add New',
|
||||
searchSort: 'Sort',
|
||||
searchTitle: 'Title',
|
||||
searchWebsite: 'Website',
|
||||
searchUser: 'User',
|
||||
searchCreated: 'Created',
|
||||
searchUpdated: 'Updated',
|
||||
searchAttachments: 'Attachments',
|
||||
searchAZ: 'A → Z',
|
||||
searchZA: 'Z → A',
|
||||
searchON: 'Old → New',
|
||||
searchNO: 'New → Old',
|
||||
searchShiftClickOr: 'shift-click or',
|
||||
searchAdvTitle: 'Toggle advanced search',
|
||||
searchSearchIn: 'Search in',
|
||||
searchOther: 'Other fields',
|
||||
searchProtect: 'Secure fields',
|
||||
searchOptions: 'Options',
|
||||
searchCase: 'Match case',
|
||||
searchRegex: 'RegEx',
|
||||
searchHistory: 'History',
|
||||
|
||||
openOpen: 'Open',
|
||||
openNew: 'New',
|
||||
openDemo: 'Demo',
|
||||
openCaps: 'Caps Lock is on',
|
||||
openKeyFile: 'key file',
|
||||
openKeyFileDropbox: '(from dropbox)',
|
||||
openDropHere: 'drop files here',
|
||||
openFailedRead: 'Failed to read file',
|
||||
openNothingFound: 'Nothing found',
|
||||
openNothingFoundBody: 'You have no files in your Dropbox which could be opened.',
|
||||
openNothingFoundBodyAppFolder: 'Files are searched inside app folder in your Dropbox.',
|
||||
openSelectFile: 'Select a file',
|
||||
openSelectFileBody: 'Select a file from your Dropbox which you would like to open',
|
||||
openPassFor: 'Password for',
|
||||
|
||||
detAttDownload: 'Shift-click attachment button to download or ',
|
||||
detAttDelToRemove: 'Delete to remove',
|
||||
detEmpty: 'Your passwords will be displayed here',
|
||||
detGroupRestore: 'To restore this group, please drag it to any group outside trash',
|
||||
detHistoryClickPoint: 'Click entry history timeline point to view state',
|
||||
detHistoryReturn: 'return to entry',
|
||||
detHistoryRevert: 'Revert to state',
|
||||
detHistoryDel: 'Delete state',
|
||||
detHistoryDiscard: 'Discard changes',
|
||||
detHistoryEmpty: 'empty',
|
||||
detHistoryModified: 'modified',
|
||||
detHistoryRec: 'record',
|
||||
detHistoryRecs: 'records',
|
||||
detHistoryVersion: 'Version',
|
||||
detHistorySaved: 'Saved',
|
||||
detHistoryTitle: 'Title',
|
||||
detHistoryNoTitle: 'no title',
|
||||
detHistoryCurState: 'current state',
|
||||
detHistoryCurUnsavedState: 'current unsaved state',
|
||||
detBackToList: 'back to list',
|
||||
detSetIconColor: 'Change icon color',
|
||||
detSetIcon: 'Change icon',
|
||||
detDropAttachments: 'drop attachments here',
|
||||
detDelEntry: 'Delete',
|
||||
detDelEntryPerm: 'Delete permanently',
|
||||
detUser: 'User',
|
||||
detPassword: 'Password',
|
||||
detWebsite: 'Website',
|
||||
detNotes: 'Notes',
|
||||
detTags: 'Tags',
|
||||
detExpires: 'Expires',
|
||||
detExpired: 'expired',
|
||||
detFile: 'File',
|
||||
detCreated: 'Created',
|
||||
detUpdated: 'Updated',
|
||||
detHistory: 'History',
|
||||
detNetField: 'New Field',
|
||||
detAddField: 'add field',
|
||||
detAttachments: 'Attachments',
|
||||
detDelFromTrash: 'Delete from trash?',
|
||||
detDelFromTrashBody: 'You will not be able to put it back.',
|
||||
detDelFromTrashBodyHint: 'To quickly remove all items from trash, click empty icon in Trash menu.',
|
||||
detPassCopied: 'Password copied',
|
||||
detPassCopiedTime: 'Password copied for {} seconds',
|
||||
detCopyHint: 'You can copy field value with click on its title',
|
||||
|
||||
appSecWarn: 'Not Secure!',
|
||||
appSecWarnBody1: 'You have loaded this app with insecure connection. ' +
|
||||
'Someone may be watching you and stealing your passwords. ' +
|
||||
'We strongly advice you to stop, unless you clearly understand what you\'re doing.',
|
||||
appSecWarnBody2: 'Yes, your database is encrypted but no one can guarantee that the app has not been modified on the way to you.',
|
||||
appSecWarnBtn: 'I understand the risks, continue',
|
||||
appUnsavedWarn: 'Unsaved changes!',
|
||||
appUnsavedWarnBody: 'You have unsaved files, if you close the app, changes will be lost.',
|
||||
appExitBtn: 'Discard changes',
|
||||
appExitSaveBtn: 'Save changes',
|
||||
appDontExitBtn: 'Don\'t exit',
|
||||
appCannotLockAutoInit: 'The app cannot be locked because auto save is disabled.',
|
||||
appCannotLock: 'You have unsaved changes that will be lost. Continue?',
|
||||
appSaveChangesBtn: 'Save changes',
|
||||
appDiscardChangesBtn: 'Discard changes',
|
||||
appAutoSave: 'Save changes automatically',
|
||||
appSaveError: 'Save Error',
|
||||
appSaveErrorBody: 'Failed to auto-save file',
|
||||
appSaveErrorBodyMul: 'Failed to auto-save files:',
|
||||
|
||||
setGenTitle: 'General Settings',
|
||||
setGenUpdate: 'Update',
|
||||
setGenNewVersion: 'New app version was released and downloaded',
|
||||
setGenReleaseNotes: 'View release notes',
|
||||
setGenReloadTpUpdate: 'Reload to update',
|
||||
setGenUpdateManual: 'New version has been released. It will check for updates and install them automatically ' +
|
||||
'but auto-upgrading from your version is impossible.',
|
||||
setGenDownloadUpdate: 'Download update',
|
||||
setGenUpdateAuto: 'Download and install automatically',
|
||||
setGenUpdateCheck: 'Check but don\'t install',
|
||||
setGenNoUpdate: 'Never check for updates',
|
||||
setGenUpdateChecking: 'Checking for updates',
|
||||
setGenCheckUpdate: 'Check for updates',
|
||||
setGenErrorChecking: 'Error checking for updates',
|
||||
setGenLastCheckSuccess: 'Last successful check was at {}',
|
||||
setGenLastCheckVer: 'the latest version was {}',
|
||||
setGenCheckedAt: 'Checked at',
|
||||
setGenLatestVer: 'you are using the latest version',
|
||||
setGenNewVer: 'new version {} available, released at',
|
||||
setGenDownloadingUpdate: 'Downloading update...',
|
||||
setGenExtractingUpdate: 'Extracting update...',
|
||||
setGenCheckErr: 'There was an error downloading new version',
|
||||
setGenNeverChecked: 'Never checked for updates',
|
||||
setGenRestartToUpdate: 'Restart to update',
|
||||
setGenDownloadAndRestart: 'Download update and restart',
|
||||
setGenAppearance: 'Appearance',
|
||||
setGenTheme: 'Theme',
|
||||
setGenShowSubgroups: 'Show entries from all subgroups',
|
||||
setGenTableView: 'Entries list table view',
|
||||
setGenColorfulIcons: 'Colorful custom icons in list',
|
||||
setGenAutoSync: 'Automatically save and sync',
|
||||
setGenLockInactive: 'Auto-lock if the app is inactive',
|
||||
setGenNoAutoLock: 'Don\'t auto-lock',
|
||||
setGenLockMinutes: 'In {} minutes',
|
||||
setGenLockHour: 'In an hour',
|
||||
setGenClearClip: 'Clear clipboard after copy',
|
||||
setGenNoClear: 'Don\'t clear',
|
||||
setGenClearSeconds: 'In {} seconds',
|
||||
setGenClearMinute: 'In a minute',
|
||||
setGenMinInstead: 'Minimize app instead of close',
|
||||
setGenLockMinimize: 'Auto-lock on minimize',
|
||||
setGenAdvanced: 'Advanced',
|
||||
setGenDevTools: 'Show dev tools',
|
||||
|
||||
setFilePath: 'File path',
|
||||
setFileStorage: 'This file is opened from {}.',
|
||||
setFileIntl: 'This file is stored in internal app storage',
|
||||
setFileLocalHint: 'Want to work seamlessly with local files?',
|
||||
setFileDownloadApp: 'Download a desktop app',
|
||||
setFileSave: 'Save',
|
||||
setFileSyncWith: 'Sync with {}',
|
||||
setFileSaveFile: 'Save to file',
|
||||
setFileExportXml: 'Export to XML',
|
||||
setFileClose: 'Close',
|
||||
setFileSync: 'Sync',
|
||||
setFileLastSync: 'Last sync',
|
||||
setFileLastSyncUnknown: 'unknown',
|
||||
setFileSyncInProgress: 'sync in progress',
|
||||
setFileSyncError: 'Sync error',
|
||||
setFileSettings: 'Settings',
|
||||
setFilePass: 'Master password',
|
||||
setFilePassChanged: 'password was changed; leave the field blank to use old password',
|
||||
setFileKeyFile: 'Key file',
|
||||
setFileSelKeyFile: 'Select a key file',
|
||||
setFileNames: 'Names',
|
||||
setFileName: 'Name',
|
||||
setFileDefUser: 'Default username',
|
||||
setFileHistory: 'History',
|
||||
setFileEnableTrash: 'Enable trash',
|
||||
setFileHistLen: 'History length, keep last records per entry',
|
||||
resFileHistSize: 'History size, total MB per file',
|
||||
setFileAdvanced: 'Advanced',
|
||||
setFileRounds: 'Key encryption rounds',
|
||||
setFileUseKeyFile: 'Use key file',
|
||||
setFileUseGenKeyFile: 'Use generated key file',
|
||||
setFileUseOldKeyFile: 'Use old key file',
|
||||
setFileGenKeyFile: 'Generate new key file',
|
||||
setFileDontUseKeyFile: 'Don\'t use key file',
|
||||
setFileEmptyPass: 'Empty password',
|
||||
setFileEmptyPassBody: 'Saving database with empty password makes it completely unprotected. Do you really want to do it?',
|
||||
setFileSaveError: 'Save error',
|
||||
setFileSaveErrorBody: 'Error saving to file',
|
||||
setFileAlreadyExists: 'Already exists',
|
||||
setFileAlreadyExistsBody: 'File {} already exists in your Dropbox. Overwrite it?',
|
||||
setFileUnsaved: 'Unsaved changes',
|
||||
setFileUnsavedBody: 'There are unsaved changes in this file',
|
||||
setFileCloseNoSave: 'Close and lose changes',
|
||||
setFileDontClose: 'Don\'t close',
|
||||
|
||||
setShTitle: 'Shortcuts',
|
||||
setShShowAll: 'show all items',
|
||||
setShColors: 'show items with colors',
|
||||
setShTrash: 'go to trash',
|
||||
setShFind: 'search, or just start typing',
|
||||
setShClearSearch: 'clear search',
|
||||
setShEntry: 'go to entry',
|
||||
setShCopy: 'copy password or selected field',
|
||||
setShPrev: 'go to previous item',
|
||||
setShNext: 'go to next item',
|
||||
setShCreateEntry: 'create entry',
|
||||
setShOpen: 'open / new',
|
||||
setShSave: 'save all files',
|
||||
setShGen: 'generate password',
|
||||
|
||||
setAboutTitle: 'About',
|
||||
setAboutBuilt: 'This app is built with these awesome tools',
|
||||
setAboutLic: 'License',
|
||||
setAboutLicComment: 'The app itself and all included components which are not in public domain are licensed under MIT license',
|
||||
setAboutFirst: 'This is an open-source app created by {}',
|
||||
setAboutSecond: ' and licensed under {}.',
|
||||
setAboutSource: 'The source code and issues are on {}.',
|
||||
|
||||
setHelpTitle: 'Help',
|
||||
setHelpFormat: 'File Format',
|
||||
setHelpFormatBody: 'This is a port of {} app built with web technologies. ' +
|
||||
'It understands files in KeePass format (kdbx). You can create such files (password databases) either in KeePass, ' +
|
||||
'or in this app. The file format is 100% compatible and should be understood by both apps.',
|
||||
setHelpProblems: 'Problems?',
|
||||
setHelpProblems1: 'If something goes wrong, please {} ',
|
||||
setHelpProblems2: 'or {}',
|
||||
setHelpOpenIssue: 'open an issue on GitHub',
|
||||
setHelpContactLink: 'contact a developer directly',
|
||||
setHelpAppInfo: 'App information',
|
||||
setHelpOtherPlatforms: 'Other platforms',
|
||||
setHelpDesktopApps: 'Desktop apps',
|
||||
setHelpWebApp: 'Web app',
|
||||
setHelpUpdates: 'Updates',
|
||||
setHelpTwitter: 'App twitter',
|
||||
|
||||
dropboxNotConfigured: 'Dropbox not configured',
|
||||
dropboxNotConfiguredBody1: 'So, you are using KeeWeb on your own server? Good!',
|
||||
dropboxNotConfiguredBody2: '{} is required to make Dropbox work, it\'s just 3 steps away.',
|
||||
dropboxNotConfiguredLink: 'Some configuration',
|
||||
dropboxLogin: 'Dropbox Login',
|
||||
dropboxLoginBody: 'To continue, you have to sign in to Dropbox.',
|
||||
dropboxSyncError: 'Dropbox Sync Error',
|
||||
dropboxNotFoundBody: 'The file was not found. Has it been removed from another computer?',
|
||||
dropboxFull: 'Dropbox Full',
|
||||
dropboxFullBody: 'Your Dropbox is full, there\'s no space left anymore.',
|
||||
dropboxRateLimitedBody: 'Too many requests to Dropbox have been made by this app. Please, try again later.',
|
||||
dropboxNetError: 'Dropbox Sync Network Error',
|
||||
dropboxNetErrorBody: 'Network error occured during Dropbox sync. Please, check your connection and try again.',
|
||||
dropboxErrorBody: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ',
|
||||
dropboxErrorRepeatBody: 'Something went wrong during Dropbox sync. Please, try again later. Error: ',
|
||||
|
||||
launcherSave: 'Save Passwords Database',
|
||||
launcherFileFilter: 'KeePass files'
|
||||
};
|
||||
|
||||
module.exports = Locale;
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
'use strict';
|
||||
|
||||
var FeatureDetector = require('./feature-detector');
|
||||
|
||||
var Tip = function(el, config) {
|
||||
this.el = el;
|
||||
this.title = config && config.title || el.attr('title');
|
||||
this.placement = config && config.placement || el.attr('tip-placement');
|
||||
this.fast = config && config.fast || false;
|
||||
this.tipEl = null;
|
||||
this.showTimeout = null;
|
||||
this.hideTimeout = null;
|
||||
};
|
||||
|
||||
Tip.enabled = FeatureDetector.isDesktop();
|
||||
|
||||
Tip.prototype.init = function() {
|
||||
if (!Tip.enabled) {
|
||||
return;
|
||||
}
|
||||
this.el.removeAttr('title');
|
||||
this.el.mouseenter(this.mouseenter.bind(this)).mouseleave(this.mouseleave.bind(this));
|
||||
this.el.click(this.mouseleave.bind(this));
|
||||
};
|
||||
|
||||
Tip.prototype.show = function() {
|
||||
if (!Tip.enabled) {
|
||||
return;
|
||||
}
|
||||
if (this.tipEl) {
|
||||
this.tipEl.remove();
|
||||
if (this.hideTimeout) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = null;
|
||||
}
|
||||
}
|
||||
var tipEl = this.tipEl = $('<div></div>').addClass('tip').appendTo('body').html(this.title);
|
||||
var rect = this.el[0].getBoundingClientRect(),
|
||||
tipRect = this.tipEl[0].getBoundingClientRect();
|
||||
var placement = this.placement || this.getAutoPlacement(rect, tipRect);
|
||||
tipEl.addClass('tip--' + placement);
|
||||
if (this.fast) {
|
||||
tipEl.addClass('tip--fast');
|
||||
}
|
||||
var top, left;
|
||||
var offset = 10;
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
top = rect.top - tipRect.height - offset;
|
||||
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
||||
break;
|
||||
case 'bottom':
|
||||
top = rect.bottom + offset;
|
||||
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
||||
break;
|
||||
case 'left':
|
||||
top = rect.top + rect.height / 2 - tipRect.height / 2;
|
||||
left = rect.left - tipRect.width - offset;
|
||||
break;
|
||||
case 'right':
|
||||
top = rect.top + rect.height / 2 - tipRect.height / 2;
|
||||
left = rect.right + offset;
|
||||
break;
|
||||
}
|
||||
tipEl.css({ top: top, left: left });
|
||||
};
|
||||
|
||||
Tip.prototype.hide = function() {
|
||||
if (this.tipEl) {
|
||||
this.tipEl.remove();
|
||||
this.tipEl = null;
|
||||
}
|
||||
};
|
||||
|
||||
Tip.prototype.mouseenter = function() {
|
||||
var that = this;
|
||||
if (this.showTimeout) {
|
||||
return;
|
||||
}
|
||||
this.showTimeout = setTimeout(function() {
|
||||
that.showTimeout = null;
|
||||
that.show();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
Tip.prototype.mouseleave = function() {
|
||||
var that = this;
|
||||
if (this.tipEl) {
|
||||
that.tipEl.addClass('tip--hide');
|
||||
this.hideTimeout = setTimeout(function () {
|
||||
that.hideTimeout = null;
|
||||
that.hide();
|
||||
}, 500);
|
||||
}
|
||||
if (this.showTimeout) {
|
||||
clearTimeout(this.showTimeout);
|
||||
this.showTimeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
Tip.prototype.getAutoPlacement = function(rect, tipRect) {
|
||||
var padding = 20;
|
||||
var bodyRect = document.body.getBoundingClientRect();
|
||||
var canShowToBottom = bodyRect.bottom - rect.bottom > padding + tipRect.height,
|
||||
canShowToHalfRight = bodyRect.right - rect.right > padding + tipRect.width / 2,
|
||||
canShowToRight = bodyRect.right - rect.right > padding + tipRect.width,
|
||||
canShowToHalfLeft = rect.left > padding + tipRect.width / 2,
|
||||
canShowToLeft = rect.left > padding + tipRect.width;
|
||||
if (canShowToBottom) {
|
||||
if (canShowToLeft && !canShowToHalfRight) {
|
||||
return 'left';
|
||||
} else if (canShowToRight && !canShowToHalfLeft) {
|
||||
return 'right';
|
||||
} else {
|
||||
return 'bottom';
|
||||
}
|
||||
}
|
||||
if (canShowToLeft && !canShowToHalfRight) {
|
||||
return 'left';
|
||||
} else if (canShowToRight && !canShowToHalfLeft) {
|
||||
return 'right';
|
||||
} else {
|
||||
return 'top';
|
||||
}
|
||||
};
|
||||
|
||||
Tip.createTips = function(container) {
|
||||
if (!Tip.enabled) {
|
||||
return;
|
||||
}
|
||||
container.find('[title]').each(function(ix, el) {
|
||||
if (!el._tip) {
|
||||
var tip = new Tip($(el));
|
||||
tip.init();
|
||||
el._tip = tip;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Tip;
|
|
@ -10,6 +10,7 @@ var Backbone = require('backbone'),
|
|||
GrpView = require('../views/grp-view'),
|
||||
OpenView = require('../views/open-view'),
|
||||
SettingsView = require('../views/settings/settings-view'),
|
||||
KeyChangeView = require('../views/key-change-view'),
|
||||
Alerts = require('../comp/alerts'),
|
||||
Keys = require('../const/keys'),
|
||||
Timeouts = require('../const/timeouts'),
|
||||
|
@ -17,12 +18,13 @@ var Backbone = require('backbone'),
|
|||
IdleTracker = require('../comp/idle-tracker'),
|
||||
Launcher = require('../comp/launcher'),
|
||||
ThemeChanger = require('../util/theme-changer'),
|
||||
Locale = require('../util/locale'),
|
||||
UpdateModel = require('../models/update-model');
|
||||
|
||||
var AppView = Backbone.View.extend({
|
||||
el: 'body',
|
||||
|
||||
template: require('templates/app.html'),
|
||||
template: require('templates/app.hbs'),
|
||||
|
||||
events: {
|
||||
'contextmenu': 'contextmenu',
|
||||
|
@ -59,6 +61,7 @@ var AppView = Backbone.View.extend({
|
|||
this.listenTo(Backbone, 'show-file', this.showFileSettings);
|
||||
this.listenTo(Backbone, 'open-file', this.toggleOpenFile);
|
||||
this.listenTo(Backbone, 'save-all', this.saveAll);
|
||||
this.listenTo(Backbone, 'remote-key-changed', this.remoteKeyChanged);
|
||||
this.listenTo(Backbone, 'toggle-settings', this.toggleSettings);
|
||||
this.listenTo(Backbone, 'toggle-menu', this.toggleMenu);
|
||||
this.listenTo(Backbone, 'toggle-details', this.toggleDetails);
|
||||
|
@ -104,6 +107,7 @@ var AppView = Backbone.View.extend({
|
|||
this.views.footer.toggle(this.model.files.hasOpenFiles());
|
||||
this.hideSettings();
|
||||
this.hideOpenFile();
|
||||
this.hideKeyChange();
|
||||
this.views.open = new OpenView({ model: this.model });
|
||||
this.views.open.setElement(this.$el.find('.app__body')).render();
|
||||
this.views.open.on('close', this.showEntries, this);
|
||||
|
@ -142,6 +146,7 @@ var AppView = Backbone.View.extend({
|
|||
this.views.footer.show();
|
||||
this.hideOpenFile();
|
||||
this.hideSettings();
|
||||
this.hideKeyChange();
|
||||
},
|
||||
|
||||
hideOpenFile: function() {
|
||||
|
@ -159,6 +164,13 @@ var AppView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
hideKeyChange: function() {
|
||||
if (this.views.keyChange) {
|
||||
this.views.keyChange.hide();
|
||||
this.views.keyChange = null;
|
||||
}
|
||||
},
|
||||
|
||||
showSettings: function(selectedMenuItem) {
|
||||
this.model.menu.setMenu('settings');
|
||||
this.views.menu.show();
|
||||
|
@ -169,6 +181,7 @@ var AppView = Backbone.View.extend({
|
|||
this.views.details.hide();
|
||||
this.views.grp.hide();
|
||||
this.hideOpenFile();
|
||||
this.hideKeyChange();
|
||||
this.views.settings = new SettingsView({ model: this.model });
|
||||
this.views.settings.setElement(this.$el.find('.app__body')).render();
|
||||
if (!selectedMenuItem) {
|
||||
|
@ -186,6 +199,22 @@ var AppView = Backbone.View.extend({
|
|||
this.views.grp.show();
|
||||
},
|
||||
|
||||
showKeyChange: function(file) {
|
||||
if (this.views.keyChange || Alerts.alertDisplayed) {
|
||||
return;
|
||||
}
|
||||
this.views.menu.hide();
|
||||
this.views.listWrap.hide();
|
||||
this.views.list.hide();
|
||||
this.views.listDrag.hide();
|
||||
this.views.details.hide();
|
||||
this.views.grp.hide();
|
||||
this.views.keyChange = new KeyChangeView({ model: file });
|
||||
this.views.keyChange.setElement(this.$el.find('.app__body')).render();
|
||||
this.views.keyChange.on('accept', this.keyChangeAccept.bind(this));
|
||||
this.views.keyChange.on('cancel', this.showEntries.bind(this));
|
||||
},
|
||||
|
||||
fileListUpdated: function() {
|
||||
if (this.model.files.hasOpenFiles()) {
|
||||
this.showEntries();
|
||||
|
@ -222,13 +251,25 @@ var AppView = Backbone.View.extend({
|
|||
if (Launcher && !Launcher.exitRequested) {
|
||||
if (!this.exitAlertShown) {
|
||||
var that = this;
|
||||
if (this.model.settings.get('autoSave')) {
|
||||
that.saveAndExit();
|
||||
return;
|
||||
}
|
||||
that.exitAlertShown = true;
|
||||
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();
|
||||
header: Locale.appUnsavedWarn,
|
||||
body: Locale.appUnsavedWarnBody,
|
||||
buttons: [
|
||||
{result: 'save', title: Locale.appExitSaveBtn},
|
||||
{result: 'exit', title: Locale.appExitBtn, error: true},
|
||||
{result: '', title: Locale.appDontExitBtn}
|
||||
],
|
||||
success: function (result) {
|
||||
if (result === 'save') {
|
||||
that.saveAndExit();
|
||||
} else {
|
||||
Launcher.exit();
|
||||
}
|
||||
},
|
||||
cancel: function() {
|
||||
Launcher.cancelRestart(false);
|
||||
|
@ -240,7 +281,7 @@ var AppView = Backbone.View.extend({
|
|||
}
|
||||
return Launcher.preventExit(e);
|
||||
}
|
||||
return 'You have unsaved files, all changes will be lost.';
|
||||
return Locale.appUnsavedWarnBody;
|
||||
} else if (Launcher && !Launcher.exitRequested && !Launcher.restartPending &&
|
||||
Launcher.canMinimize() && this.model.settings.get('minimizeOnClose')) {
|
||||
Launcher.minimizeApp();
|
||||
|
@ -292,20 +333,19 @@ var AppView = Backbone.View.extend({
|
|||
}
|
||||
if (this.model.files.hasUnsavedFiles()) {
|
||||
if (this.model.settings.get('autoSave')) {
|
||||
this.saveAndLock(autoInit);
|
||||
this.saveAndLock();
|
||||
} else {
|
||||
var message = autoInit ? 'The app cannot be locked because auto save is disabled.'
|
||||
: 'You have unsaved changes that will be lost. Continue?';
|
||||
var message = autoInit ? Locale.appCannotLockAutoInit : Locale.appCannotLock;
|
||||
Alerts.alert({
|
||||
icon: 'lock',
|
||||
header: 'Lock',
|
||||
body: message,
|
||||
buttons: [
|
||||
{ result: 'save', title: 'Save changes' },
|
||||
{ result: 'discard', title: 'Discard changes', error: true },
|
||||
{ result: '', title: 'Cancel' }
|
||||
{ result: 'save', title: Locale.appSaveChangesBtn },
|
||||
{ result: 'discard', title: Locale.appDiscardChangesBtn, error: true },
|
||||
{ result: '', title: Locale.alertCancel }
|
||||
],
|
||||
checkbox: 'Save changes automatically',
|
||||
checkbox: Locale.appAutoSave,
|
||||
success: function(result, autoSaveChecked) {
|
||||
if (result === 'save') {
|
||||
if (autoSaveChecked) {
|
||||
|
@ -323,7 +363,7 @@ var AppView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
saveAndLock: function(/*autoInit*/) {
|
||||
saveAndLock: function(complete) {
|
||||
var pendingCallbacks = 0,
|
||||
errorFiles = [],
|
||||
that = this;
|
||||
|
@ -344,18 +384,29 @@ var AppView = Backbone.View.extend({
|
|||
if (--pendingCallbacks === 0) {
|
||||
if (errorFiles.length && that.model.files.hasDirtyFiles()) {
|
||||
if (!Alerts.alertDisplayed) {
|
||||
var alertBody = errorFiles.length > 1 ? Locale.appSaveErrorBodyMul : Locale.appSaveErrorBody;
|
||||
Alerts.error({
|
||||
header: 'Save Error',
|
||||
body: 'Failed to auto-save file' + (errorFiles.length > 1 ? 's: ' : '') + ' ' + errorFiles.join(', ')
|
||||
header: Locale.appSaveError,
|
||||
body: alertBody + ' ' + errorFiles.join(', ')
|
||||
});
|
||||
}
|
||||
if (complete) { complete(true); }
|
||||
} else {
|
||||
that.closeAllFilesAndShowFirst();
|
||||
if (complete) { complete(true); }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
saveAndExit: function() {
|
||||
this.saveAndLock(function(result) {
|
||||
if (result) {
|
||||
Launcher.exit();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
closeAllFilesAndShowFirst: function() {
|
||||
var firstFile = this.model.files.find(function(file) { return !file.get('demo') && !file.get('created'); });
|
||||
this.model.closeAllFiles();
|
||||
|
@ -379,6 +430,21 @@ var AppView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
remoteKeyChanged: function(e) {
|
||||
this.showKeyChange(e.file);
|
||||
},
|
||||
|
||||
keyChangeAccept: function(e) {
|
||||
this.showEntries();
|
||||
this.model.syncFile(e.file, {
|
||||
remoteKey: {
|
||||
password: e.password,
|
||||
keyFileName: e.keyFileName,
|
||||
keyFileData: e.keyFileData
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleSettings: function(page) {
|
||||
var menuItem = page ? this.model.menu[page + 'Section'] : null;
|
||||
if (menuItem) {
|
||||
|
@ -443,8 +509,9 @@ var AppView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
bodyClick: function() {
|
||||
bodyClick: function(e) {
|
||||
IdleTracker.regUserAction();
|
||||
Backbone.trigger('click', e);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ var Backbone = require('backbone'),
|
|||
FeatureDetector = require('../../util/feature-detector');
|
||||
|
||||
var DetailsAttachmentView = Backbone.View.extend({
|
||||
template: require('templates/details/details-attachment.html'),
|
||||
template: require('templates/details/details-attachment.hbs'),
|
||||
|
||||
events: {
|
||||
},
|
||||
|
|
|
@ -4,12 +4,13 @@ var Backbone = require('backbone'),
|
|||
KeyHandler = require('../../comp/key-handler'),
|
||||
Keys = require('../../const/keys'),
|
||||
Format = require('../../util/format'),
|
||||
Locale = require('../../util/locale'),
|
||||
Alerts = require('../../comp/alerts'),
|
||||
FieldViewReadOnly = require('../fields/field-view-read-only'),
|
||||
FieldViewReadOnlyRaw = require('../fields/field-view-read-only-raw');
|
||||
|
||||
var DetailsHistoryView = Backbone.View.extend({
|
||||
template: require('templates/details/details-history.html'),
|
||||
template: require('templates/details/details-history.hbs'),
|
||||
|
||||
events: {
|
||||
'click .details__history-close': 'closeHistory',
|
||||
|
@ -82,25 +83,26 @@ var DetailsHistoryView = Backbone.View.extend({
|
|||
this.removeFieldViews();
|
||||
this.bodyEl.html('');
|
||||
var colorCls = this.record.color ? this.record.color + '-color' : '';
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Rev', title: 'Version', value: ix + 1 } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: 'Saved',
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Rev', title: Locale.detHistoryVersion, value: ix + 1 } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: Locale.detHistorySaved,
|
||||
value: Format.dtStr(this.record.updated) +
|
||||
(this.record.unsaved ? ' (current unsaved state)' : '') +
|
||||
((ix === this.history.length - 1 && !this.record.unsaved) ? ' (current state)' : '') } }));
|
||||
this.fieldViews.push(new FieldViewReadOnlyRaw({ model: { name: '$Title', title: 'Title',
|
||||
value: '<i class="fa fa-' + this.record.icon + ' ' + colorCls + '"></i> ' + _.escape(this.record.title) || '(no title)' } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$UserName', title: 'User', value: this.record.user } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Password', title: 'Password', value: this.record.password } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$URL', title: 'Website', value: this.record.url } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Notes', title: 'Notes', value: this.record.notes } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Tags', title: 'Tags', value: this.record.tags.join(', ') } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Expires', title: 'Expires',
|
||||
value: this.record.expires ? Format.dtStr(this.record.expires) : 'Never' } }));
|
||||
(this.record.unsaved ? ' (' + Locale.detHistoryCurUnsavedState + ')' : '') +
|
||||
((ix === this.history.length - 1 && !this.record.unsaved) ? ' (' + Locale.detHistoryCurState + ')' : '') } }));
|
||||
this.fieldViews.push(new FieldViewReadOnlyRaw({ model: { name: '$Title', title: Locale.detHistoryTitle,
|
||||
value: '<i class="fa fa-' + this.record.icon + ' ' + colorCls + '"></i> ' +
|
||||
_.escape(this.record.title) || '(' + Locale.detHistoryNoTitle + ')' } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$UserName', title: Locale.detUser, value: this.record.user } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Password', title: Locale.detPassword, value: this.record.password } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$URL', title: Locale.detWebsite, value: this.record.url } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Notes', title: Locale.detNotes, value: this.record.notes } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Tags', title: Locale.detTags, value: this.record.tags.join(', ') } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Expires', title: Locale.detExpires,
|
||||
value: this.record.expires ? Format.dtStr(this.record.expires) : '' } }));
|
||||
_.forEach(this.record.fields, function(value, field) {
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$' + field, title: field, value: value } }));
|
||||
}, this);
|
||||
if (this.record.attachments.length) {
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Attachments', title: 'Attachments',
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Attachments', title: Locale.detAttachments,
|
||||
value: this.record.attachments.map(function(att) { return att.title; }).join(', ') } }));
|
||||
}
|
||||
this.fieldViews.forEach(function(fieldView) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
var Backbone = require('backbone'),
|
||||
GroupModel = require('../../models/group-model'),
|
||||
AppSettingsModel = require('../../models/app-settings-model'),
|
||||
Scrollable = require('../../mixins/scrollable'),
|
||||
FieldViewText = require('../fields/field-view-text'),
|
||||
FieldViewDate = require('../fields/field-view-date'),
|
||||
|
@ -18,17 +19,22 @@ var Backbone = require('backbone'),
|
|||
Alerts = require('../../comp/alerts'),
|
||||
CopyPaste = require('../../comp/copy-paste'),
|
||||
Format = require('../../util/format'),
|
||||
Locale = require('../../util/locale'),
|
||||
Tip = require('../../util/tip'),
|
||||
Timeouts = require('../../const/timeouts'),
|
||||
FileSaver = require('filesaver'),
|
||||
baron = require('baron'),
|
||||
kdbxweb = require('kdbxweb');
|
||||
|
||||
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'),
|
||||
template: require('templates/details/details.hbs'),
|
||||
emptyTemplate: require('templates/details/details-empty.hbs'),
|
||||
groupTemplate: require('templates/details/details-group.hbs'),
|
||||
|
||||
fieldViews: null,
|
||||
views: null,
|
||||
passEditView: null,
|
||||
addNewFieldView: null,
|
||||
passCopyTip: null,
|
||||
|
||||
events: {
|
||||
'click .details__colors-popup-item': 'selectColor',
|
||||
|
@ -65,6 +71,10 @@ var DetailsView = Backbone.View.extend({
|
|||
removeFieldViews: function() {
|
||||
this.fieldViews.forEach(function(fieldView) { fieldView.remove(); });
|
||||
this.fieldViews = [];
|
||||
if (this.passCopyTip) {
|
||||
this.passCopyTip.hide();
|
||||
this.passCopyTip = null;
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
|
@ -79,57 +89,57 @@ var DetailsView = Backbone.View.extend({
|
|||
}
|
||||
if (this.model instanceof GroupModel) {
|
||||
this.$el.html(this.groupTemplate());
|
||||
Tip.createTips(this.$el);
|
||||
return;
|
||||
}
|
||||
var model = $.extend({ deleted: this.appModel.filter.trash }, this.model);
|
||||
this.$el.html(this.template(model));
|
||||
Tip.createTips(this.$el);
|
||||
this.setSelectedColor(this.model.color);
|
||||
this.addFieldViews();
|
||||
this.scroll = baron({
|
||||
this.createScroll({
|
||||
root: this.$el.find('.details__body')[0],
|
||||
scroller: this.$el.find('.scroller')[0],
|
||||
bar: this.$el.find('.scroller__bar')[0],
|
||||
$: Backbone.$
|
||||
bar: this.$el.find('.scroller__bar')[0]
|
||||
});
|
||||
this.scroller = this.$el.find('.scroller');
|
||||
this.scrollerBar = this.$el.find('.scroller__bar');
|
||||
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
|
||||
this.$el.find('.details').removeClass('details--drag');
|
||||
this.dragging = false;
|
||||
if (this.dragTimeout) {
|
||||
clearTimeout(this.dragTimeout);
|
||||
}
|
||||
this.pageResized();
|
||||
this.showCopyTip();
|
||||
return this;
|
||||
},
|
||||
|
||||
addFieldViews: function() {
|
||||
var model = this.model;
|
||||
this.fieldViews.push(new FieldViewText({ model: { name: '$UserName', title: 'User',
|
||||
this.fieldViews.push(new FieldViewText({ model: { name: '$UserName', title: Locale.detUser,
|
||||
value: function() { return model.user; } } }));
|
||||
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',
|
||||
this.passEditView = new FieldViewText({ model: { name: '$Password', title: Locale.detPassword, canGen: true,
|
||||
value: function() { return model.password; } } });
|
||||
this.fieldViews.push(this.passEditView);
|
||||
this.fieldViews.push(new FieldViewUrl({ model: { name: '$URL', title: Locale.detWebsite,
|
||||
value: function() { return model.url; } } }));
|
||||
this.fieldViews.push(new FieldViewText({ model: { name: '$Notes', title: 'Notes', multiline: 'true',
|
||||
this.fieldViews.push(new FieldViewText({ model: { name: '$Notes', title: Locale.detNotes, multiline: 'true',
|
||||
value: function() { return model.notes; } } }));
|
||||
this.fieldViews.push(new FieldViewTags({ model: { name: 'Tags', title: 'Tags', tags: this.appModel.tags,
|
||||
this.fieldViews.push(new FieldViewTags({ model: { name: 'Tags', title: Locale.detTags, tags: this.appModel.tags,
|
||||
value: function() { return model.tags; } } }));
|
||||
this.fieldViews.push(new FieldViewDate({ model: { name: 'Expires', title: 'Expires', lessThanNow: '(expired)',
|
||||
this.fieldViews.push(new FieldViewDate({ model: { name: 'Expires', title: Locale.detExpires, lessThanNow: '(' + Locale.detExpired + ')',
|
||||
value: function() { return model.expires; } } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: 'File',
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: Locale.detFile,
|
||||
value: function() { return model.fileName; } } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Created', title: 'Created',
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Created', title: Locale.detCreated,
|
||||
value: function() { return Format.dtStr(model.created); } } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: 'Updated',
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: Locale.detUpdated,
|
||||
value: function() { return Format.dtStr(model.updated); } } }));
|
||||
this.fieldViews.push(new FieldViewHistory({ model: { name: 'History', title: 'History',
|
||||
this.fieldViews.push(new FieldViewHistory({ model: { name: 'History', title: Locale.detHistory,
|
||||
value: function() { return { length: model.historyLength, unsaved: model.unsaved }; } } }));
|
||||
_.forEach(model.fields, function(value, field) {
|
||||
this.fieldViews.push(new FieldViewCustom({ model: { name: '$' + field, title: field,
|
||||
value: function() { return model.fields[field]; } } }));
|
||||
}, this);
|
||||
var newFieldTitle = 'New Field';
|
||||
var newFieldTitle = Locale.detNetField;
|
||||
if (model.fields[newFieldTitle]) {
|
||||
for (var i = 1; ; i++) {
|
||||
var newFieldTitleVariant = newFieldTitle + i;
|
||||
|
@ -139,8 +149,9 @@ var DetailsView = Backbone.View.extend({
|
|||
}
|
||||
}
|
||||
}
|
||||
this.fieldViews.push(new FieldViewCustom({ model: { name: '', title: 'add field', newField: newFieldTitle,
|
||||
value: function() { return ''; } } }));
|
||||
this.addNewFieldView = new FieldViewCustom({ model: { name: '$', title: Locale.detAddField, newField: newFieldTitle,
|
||||
value: function() { return ''; } } });
|
||||
this.fieldViews.push(this.addNewFieldView);
|
||||
|
||||
var fieldsMainEl = this.$el.find('.details__body-fields');
|
||||
var fieldsAsideEl = this.$el.find('.details__body-aside');
|
||||
|
@ -150,13 +161,6 @@ var DetailsView = Backbone.View.extend({
|
|||
}, this);
|
||||
},
|
||||
|
||||
getEditedField: function() {
|
||||
var edited = _.find(this.fieldViews, function(fieldView) {
|
||||
return fieldView.editing;
|
||||
});
|
||||
return edited ? edited.model.name : undefined;
|
||||
},
|
||||
|
||||
setSelectedColor: function(color) {
|
||||
this.$el.find('.details__colors-popup > .details__colors-popup-item').removeClass('details__colors-popup-item--active');
|
||||
var colorEl = this.$el.find('.details__header-color')[0];
|
||||
|
@ -267,22 +271,59 @@ var DetailsView = Backbone.View.extend({
|
|||
copyKeyPress: function() { // TODO: fix this in Safari
|
||||
if (!window.getSelection().toString()) {
|
||||
var pw = this.model.password;
|
||||
var password = pw.getText ? pw.getText() : pw;
|
||||
var password = pw.isProtected ? pw.getText() : pw;
|
||||
CopyPaste.createHiddenInput(password);
|
||||
CopyPaste.copied();
|
||||
var clipboardTime = CopyPaste.copied();
|
||||
if (!this.passCopyTip) {
|
||||
var passLabel = this.passEditView.labelEl;
|
||||
var msg = clipboardTime ? Locale.detPassCopiedTime.replace('{}', clipboardTime)
|
||||
: Locale.detPassCopied;
|
||||
var tip = new Tip(passLabel, { title: msg, placement: 'right', fast: true });
|
||||
this.passCopyTip = tip;
|
||||
tip.show();
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
tip.hide();
|
||||
that.passCopyTip = null;
|
||||
}, Timeouts.CopyTip);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
showCopyTip: function() {
|
||||
if (this.helpTipCopyShown) {
|
||||
return;
|
||||
}
|
||||
this.helpTipCopyShown = AppSettingsModel.instance.get('helpTipCopyShown');
|
||||
if (this.helpTipCopyShown) {
|
||||
return;
|
||||
}
|
||||
AppSettingsModel.instance.set('helpTipCopyShown', true);
|
||||
this.helpTipCopyShown = true;
|
||||
var newFieldLabel = this.addNewFieldView.labelEl;
|
||||
var tip = new Tip(newFieldLabel, { title: Locale.detCopyHint, placement: 'right' });
|
||||
tip.show();
|
||||
setTimeout(function() { tip.hide(); }, Timeouts.AutoHideHint);
|
||||
},
|
||||
|
||||
fieldChanged: function(e) {
|
||||
if (e.field) {
|
||||
if (e.field[0] === '$') {
|
||||
var fieldName = e.field.substr(1);
|
||||
if (e.title) {
|
||||
this.model.setField(fieldName, undefined);
|
||||
this.model.setField(e.title, e.val);
|
||||
if (e.newField && e.newField !== fieldName) {
|
||||
if (fieldName) {
|
||||
this.model.setField(fieldName, undefined);
|
||||
}
|
||||
fieldName = e.newField;
|
||||
var i = 0;
|
||||
while (this.model.hasField(fieldName)) {
|
||||
i++;
|
||||
fieldName = e.newField + i;
|
||||
}
|
||||
this.model.setField(fieldName, e.val);
|
||||
this.entryUpdated();
|
||||
return;
|
||||
} else {
|
||||
} else if (fieldName) {
|
||||
this.model.setField(fieldName, e.val);
|
||||
}
|
||||
} else if (e.field === 'Tags') {
|
||||
|
@ -304,15 +345,6 @@ var DetailsView = Backbone.View.extend({
|
|||
fieldView.update();
|
||||
}
|
||||
}, this);
|
||||
} else if (e.newField && e.val) {
|
||||
var field = e.newField;
|
||||
var i = 0;
|
||||
while (this.model.hasField(field)) {
|
||||
i++;
|
||||
field = e.newField + i;
|
||||
}
|
||||
this.model.setField(field, e.val);
|
||||
this.entryUpdated();
|
||||
}
|
||||
if (e.tab) {
|
||||
this.focusNextField(e.tab);
|
||||
|
@ -486,8 +518,8 @@ var DetailsView = Backbone.View.extend({
|
|||
|
||||
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>',
|
||||
header: Locale.detDelFromTrash,
|
||||
body: Locale.detDelFromTrashBody + ' <p class="muted-color">' + Locale.detDelFromTrashBodyHint + '</p>',
|
||||
icon: 'minus-circle',
|
||||
success: (function() {
|
||||
this.model.deleteFromTrash();
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
var Backbone = require('backbone');
|
||||
|
||||
var DropdownView = Backbone.View.extend({
|
||||
template: require('templates/dropdown.html'),
|
||||
template: require('templates/dropdown.hbs'),
|
||||
|
||||
events: {
|
||||
'click .dropdown__item': 'itemClick'
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
var FieldViewText = require('./field-view-text'),
|
||||
var Backbone = require('backbone'),
|
||||
FieldViewText = require('./field-view-text'),
|
||||
FieldView = require('./field-view'),
|
||||
Keys = require('../../const/keys'),
|
||||
Locale = require('../../util/locale'),
|
||||
kdbxweb = require('kdbxweb');
|
||||
|
||||
var FieldViewCustom = FieldViewText.extend({
|
||||
|
@ -12,39 +14,35 @@ var FieldViewCustom = FieldViewText.extend({
|
|||
|
||||
initialize: function() {
|
||||
_.extend(this.events, FieldViewText.prototype.events);
|
||||
this.model.newFieldInitial = this.model.newField;
|
||||
},
|
||||
|
||||
startEdit: function() {
|
||||
FieldViewText.prototype.startEdit.call(this);
|
||||
if (this.model.newField) {
|
||||
if (this.model.newField && this.model.title === Locale.detAddField) {
|
||||
this.model.title = this.model.newField;
|
||||
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)
|
||||
this.$el.toggleClass('details__field--protected', this.isProtected);
|
||||
$('<div/>').addClass('details__field-value-btn details__field-value-btn-protect')
|
||||
.appendTo(this.valueEl)
|
||||
.mousedown(this.protectBtnClick.bind(this));
|
||||
},
|
||||
|
||||
endEdit: function(newVal, extra) {
|
||||
if (this.model.newField && !newVal) {
|
||||
this.model.newField = this.model.newFieldInitial;
|
||||
this.$el.find('.details__field-label').text(this.model.title);
|
||||
this.$el.find('.details__field-value').text('');
|
||||
this.value = '';
|
||||
this.$el.removeClass('details__field--can-edit-title');
|
||||
extra = _.extend({}, extra);
|
||||
if (this.model.titleChanged || this.model.newField) {
|
||||
extra.newField = this.model.title;
|
||||
}
|
||||
if (!this.model.newField) {
|
||||
this.$el.removeClass('details__field--can-edit-title');
|
||||
}
|
||||
extra = _.extend({}, extra, { newField: this.model.newField });
|
||||
if (!this.editing) {
|
||||
return;
|
||||
}
|
||||
delete this.input;
|
||||
this.stopListening(Backbone, 'click', this.fieldValueBlur);
|
||||
if (typeof newVal === 'string') {
|
||||
newVal = $.trim(newVal);
|
||||
if (this.isProtected) {
|
||||
|
@ -52,16 +50,22 @@ var FieldViewCustom = FieldViewText.extend({
|
|||
}
|
||||
}
|
||||
FieldView.prototype.endEdit.call(this, newVal, extra);
|
||||
if (!newVal && this.model.newField) {
|
||||
this.model.title = Locale.detAddField;
|
||||
this.$el.find('.details__field-label').text(this.model.title);
|
||||
}
|
||||
if (this.model.titleChanged) {
|
||||
delete this.model.titleChanged;
|
||||
}
|
||||
},
|
||||
|
||||
startEditTitle: function() {
|
||||
var text = this.model.newField ? this.model.newField !== this.model.newFieldInitial ? this.model.newField : '' : this.model.title;
|
||||
startEditTitle: function(emptyTitle) {
|
||||
var text = emptyTitle ? '' : this.model.title || '';
|
||||
this.labelInput = $('<input/>');
|
||||
this.labelEl.html('').append(this.labelInput);
|
||||
this.labelInput.attr({ autocomplete: 'off', spellcheck: 'false' })
|
||||
.val(text).focus()[0].setSelectionRange(text.length, text.length);
|
||||
this.labelInput.bind({
|
||||
blur: this.fieldLabelBlur.bind(this),
|
||||
input: this.fieldLabelInput.bind(this),
|
||||
keydown: this.fieldLabelKeydown.bind(this),
|
||||
keypress: this.fieldLabelInput.bind(this),
|
||||
|
@ -71,56 +75,42 @@ var FieldViewCustom = FieldViewText.extend({
|
|||
},
|
||||
|
||||
endEditTitle: function(newTitle) {
|
||||
if (this.model.newField) {
|
||||
if (newTitle) {
|
||||
this.model.newField = newTitle;
|
||||
this.edit();
|
||||
} else {
|
||||
this.endEdit();
|
||||
}
|
||||
} else {
|
||||
this.$el.find('.details__field-label').text(this.model.title);
|
||||
this.endEdit();
|
||||
if (newTitle && newTitle !== this.model.title) {
|
||||
this.trigger('change', { field: this.model.name, title: newTitle, val: this.model.value() });
|
||||
}
|
||||
if (newTitle && newTitle !== this.model.title) {
|
||||
this.model.title = newTitle;
|
||||
this.model.titleChanged = true;
|
||||
}
|
||||
this.$el.find('.details__field-label').text(this.model.title);
|
||||
delete this.labelInput;
|
||||
if (this.editing && this.input) {
|
||||
this.input.focus();
|
||||
}
|
||||
},
|
||||
|
||||
fieldLabelClick: function(e) {
|
||||
e.stopImmediatePropagation();
|
||||
if (this.model.newField || this.editing) {
|
||||
if (this.editing) {
|
||||
this.startEditTitle();
|
||||
} else if (this.model.newField) {
|
||||
this.edit();
|
||||
this.startEditTitle(true);
|
||||
} else {
|
||||
FieldViewText.prototype.fieldLabelClick.call(this, e);
|
||||
}
|
||||
},
|
||||
|
||||
fieldLabelMousedown: function() {
|
||||
if (this.editing || this.model.newField) {
|
||||
if (this.editing) {
|
||||
this.editing = false;
|
||||
this.value = this.input.val();
|
||||
this.input.unbind('blur');
|
||||
delete this.input;
|
||||
this.valueEl.html(this.renderValue(this.value));
|
||||
this.$el.removeClass('details__field--edit');
|
||||
}
|
||||
_.delay(this.startEditTitle.bind(this));
|
||||
fieldLabelMousedown: function(e) {
|
||||
if (this.editing) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
fieldValueBlur: function(e) {
|
||||
if (this.protectJustChanged) {
|
||||
this.protectJustChanged = false;
|
||||
e.target.focus();
|
||||
return;
|
||||
fieldValueBlur: function() {
|
||||
if (this.labelInput) {
|
||||
this.endEditTitle(this.labelInput.val());
|
||||
}
|
||||
if (this.input) {
|
||||
this.endEdit(this.input.val());
|
||||
}
|
||||
this.endEdit(e.target.value);
|
||||
},
|
||||
|
||||
fieldLabelBlur: function(e) {
|
||||
this.endEditTitle(e.target.value);
|
||||
},
|
||||
|
||||
fieldLabelInput: function(e) {
|
||||
|
@ -132,25 +122,33 @@ var FieldViewCustom = FieldViewText.extend({
|
|||
},
|
||||
|
||||
fieldLabelKeydown: function(e) {
|
||||
e.stopPropagation();
|
||||
var code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_RETURN) {
|
||||
$(e.target).unbind('blur');
|
||||
this.endEditTitle(e.target.value);
|
||||
} else if (code === Keys.DOM_VK_ESCAPE) {
|
||||
$(e.target).unbind('blur');
|
||||
this.endEditTitle();
|
||||
} else if (code === Keys.DOM_VK_TAB) {
|
||||
e.preventDefault();
|
||||
$(e.target).unbind('blur');
|
||||
this.endEditTitle(e.target.value);
|
||||
}
|
||||
},
|
||||
|
||||
fieldValueInputClick: function() {
|
||||
if (this.labelInput) {
|
||||
this.endEditTitle(this.labelInput.val());
|
||||
}
|
||||
FieldViewText.prototype.fieldValueInputClick.call(this);
|
||||
},
|
||||
|
||||
protectBtnClick: function(e) {
|
||||
e.stopPropagation();
|
||||
this.isProtected = !this.isProtected;
|
||||
this.protectBtn.toggleClass('details__field-value-btn-protect--protected', this.isProtected);
|
||||
this.protectJustChanged = true;
|
||||
this.$el.toggleClass('details__field--protected', this.isProtected);
|
||||
if (this.labelInput) {
|
||||
this.endEditTitle(this.labelInput.val());
|
||||
}
|
||||
this.setTimeout(function() { this.input.focus(); });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -30,9 +30,9 @@ var FieldViewDate = FieldViewText.extend({
|
|||
i18n: {
|
||||
previousMonth: '',
|
||||
nextMonth: '',
|
||||
months: Locale.Months,
|
||||
weekdays: Locale.Weekdays,
|
||||
weekdaysShort: Locale.WeekdaysShort
|
||||
months: Locale.months,
|
||||
weekdays: Locale.weekdays,
|
||||
weekdaysShort: Locale.weekdaysShort
|
||||
}
|
||||
});
|
||||
_.defer(this.picker.show.bind(this.picker));
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
var FieldView = require('./field-view');
|
||||
var FieldView = require('./field-view'),
|
||||
Locale = require('../../util/locale');
|
||||
|
||||
var FieldViewHistory = FieldView.extend({
|
||||
renderValue: function(value) {
|
||||
if (!value.length) {
|
||||
return 'empty';
|
||||
return Locale.detHistoryEmpty;
|
||||
}
|
||||
var text = value.length + ' record' + (value.length % 10 === 1 ? '' : 's');
|
||||
var text = value.length + ' ' + (value.length % 10 === 1 ? Locale.detHistoryRec : Locale.detHistoryRecs);
|
||||
if (value.unsaved) {
|
||||
text += ' (modified)';
|
||||
text += ' (' + Locale.detHistoryModified + ')';
|
||||
}
|
||||
return '<a class="details__history-link">' + text + '</a>';
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@ var FieldView = require('./field-view');
|
|||
|
||||
var FieldViewReadOnly = FieldView.extend({
|
||||
renderValue: function(value) {
|
||||
return typeof value.byteLength === 'number' ? new Array(value.byteLength + 1).join('•') : _.escape(value);
|
||||
return value.isProtected ? new Array(value.textLength + 1).join('•') : _.escape(value);
|
||||
},
|
||||
|
||||
readonly: true
|
||||
|
|
|
@ -22,14 +22,6 @@ var FieldViewTags = FieldViewText.extend({
|
|||
},
|
||||
|
||||
endEdit: function(newVal, extra) {
|
||||
if (this.selectedTag) {
|
||||
newVal += (newVal ? ', ' : '') + this.selectedTag;
|
||||
this.input.val(newVal);
|
||||
this.input.focus();
|
||||
this.setTags();
|
||||
delete this.selectedTag;
|
||||
return;
|
||||
}
|
||||
if (newVal !== undefined) {
|
||||
newVal = this.valueToTags(newVal);
|
||||
}
|
||||
|
@ -61,8 +53,10 @@ var FieldViewTags = FieldViewText.extend({
|
|||
|
||||
getAvailableTags: function() {
|
||||
var tags = this.valueToTags(this.input.val());
|
||||
var last = tags[tags.length - 1];
|
||||
var isLastPart = last && this.model.tags.indexOf(last) < 0;
|
||||
return this.model.tags.filter(function(tag) {
|
||||
return tags.indexOf(tag) < 0;
|
||||
return tags.indexOf(tag) < 0 && (!isLastPart || tag.toLowerCase().indexOf(last.toLowerCase()) >= 0);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -78,8 +72,24 @@ var FieldViewTags = FieldViewText.extend({
|
|||
tagsAutocompleteClick: function(e) {
|
||||
e.stopPropagation();
|
||||
if (e.target.classList.contains('details__tags-autocomplete-tag')) {
|
||||
this.selectedTag = $(e.target).text();
|
||||
var selectedTag = $(e.target).text(), newVal = this.input.val();
|
||||
if (newVal) {
|
||||
var tags = this.valueToTags(newVal);
|
||||
var last = tags[tags.length - 1];
|
||||
var isLastPart = last && this.model.tags.indexOf(last) < 0;
|
||||
if (isLastPart) {
|
||||
newVal = newVal.substr(0, newVal.lastIndexOf(last)) + selectedTag;
|
||||
} else {
|
||||
newVal += ', ' + selectedTag;
|
||||
}
|
||||
} else {
|
||||
newVal = selectedTag;
|
||||
}
|
||||
this.input.val(newVal);
|
||||
this.input.focus();
|
||||
this.setTags();
|
||||
}
|
||||
this.afterPaint(function() { this.input.focus(); });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var FieldView = require('./field-view'),
|
||||
var Backbone = require('backbone'),
|
||||
FieldView = require('./field-view'),
|
||||
GeneratorView = require('../generator-view'),
|
||||
KeyHandler = require('../../comp/key-handler'),
|
||||
Keys = require('../../const/keys'),
|
||||
|
@ -9,27 +10,30 @@ var FieldView = require('./field-view'),
|
|||
|
||||
var FieldViewText = FieldView.extend({
|
||||
renderValue: function(value) {
|
||||
return value && typeof value.byteLength === 'number' ? PasswordGenerator.present(value.byteLength) :
|
||||
return value && value.isProtected ? PasswordGenerator.present(value.textLength) :
|
||||
_.escape(value || '').replace(/\n/g, '<br/>');
|
||||
},
|
||||
|
||||
getEditValue: function(value) {
|
||||
return value && value.getText ? value.getText() : value || '';
|
||||
return value && value.isProtected ? value.getText() : value || '';
|
||||
},
|
||||
|
||||
startEdit: function() {
|
||||
var text = this.getEditValue(this.value);
|
||||
var isProtected = !!(this.value && this.value.isProtected);
|
||||
this.$el.toggleClass('details__field--protected', isProtected);
|
||||
this.input = $(document.createElement(this.model.multiline ? 'textarea' : 'input'));
|
||||
this.valueEl.html('').append(this.input);
|
||||
this.input.attr({ autocomplete: 'off', spellcheck: 'false' })
|
||||
.val(text).focus()[0].setSelectionRange(text.length, text.length);
|
||||
this.input.bind({
|
||||
blur: this.fieldValueBlur.bind(this),
|
||||
input: this.fieldValueInput.bind(this),
|
||||
keydown: this.fieldValueKeydown.bind(this),
|
||||
keypress: this.fieldValueInput.bind(this),
|
||||
click: this.fieldValueInputClick.bind(this)
|
||||
click: this.fieldValueInputClick.bind(this),
|
||||
mousedown: this.fieldValueInputMouseDown.bind(this)
|
||||
});
|
||||
this.listenTo(Backbone, 'click', this.fieldValueBlur);
|
||||
if (this.model.multiline) {
|
||||
this.setInputHeight();
|
||||
}
|
||||
|
@ -92,9 +96,9 @@ var FieldViewText = FieldView.extend({
|
|||
this.input.height(newHeight);
|
||||
},
|
||||
|
||||
fieldValueBlur: function(e) {
|
||||
if (!this.gen) {
|
||||
this.endEdit(e.target.value);
|
||||
fieldValueBlur: function() {
|
||||
if (!this.gen && this.input) {
|
||||
this.endEdit(this.input.val());
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -111,21 +115,25 @@ var FieldViewText = FieldView.extend({
|
|||
}
|
||||
},
|
||||
|
||||
fieldValueInputMouseDown: function(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
fieldValueKeydown: function(e) {
|
||||
KeyHandler.reg();
|
||||
e.stopPropagation();
|
||||
var code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_RETURN) {
|
||||
if (!this.model.multiline || (!e.altKey && !e.shiftKey)) {
|
||||
$(e.target).unbind('blur');
|
||||
this.stopListening(Backbone, 'click', this.fieldValueBlur);
|
||||
this.endEdit(e.target.value);
|
||||
}
|
||||
} else if (code === Keys.DOM_VK_ESCAPE) {
|
||||
$(e.target).unbind('blur');
|
||||
this.stopListening(Backbone, 'click', this.fieldValueBlur);
|
||||
this.endEdit();
|
||||
} else if (code === Keys.DOM_VK_TAB) {
|
||||
e.preventDefault();
|
||||
$(e.target).unbind('blur');
|
||||
this.stopListening(Backbone, 'click', this.fieldValueBlur);
|
||||
this.endEdit(e.target.value, { tab: { field: this.model.name, prev: e.shiftKey } });
|
||||
}
|
||||
},
|
||||
|
@ -138,6 +146,7 @@ var FieldViewText = FieldView.extend({
|
|||
return;
|
||||
}
|
||||
delete this.input;
|
||||
this.stopListening(Backbone, 'click', this.fieldValueBlur);
|
||||
if (typeof newVal === 'string' && this.value instanceof kdbxweb.ProtectedValue) {
|
||||
newVal = kdbxweb.ProtectedValue.fromString(newVal);
|
||||
}
|
||||
|
|
|
@ -3,12 +3,18 @@
|
|||
var FieldViewText = require('./field-view-text');
|
||||
|
||||
var FieldViewUrl = FieldViewText.extend({
|
||||
displayUrlRegex: /^http:\/\//i,
|
||||
|
||||
renderValue: function(value) {
|
||||
return value ? '<a href="' + _.escape(this.fixUrl(value)) + '" target="_blank">' + _.escape(value) + '</a>' : '';
|
||||
return value ? '<a href="' + _.escape(this.fixUrl(value)) + '" target="_blank">' + _.escape(this.displayUrl(value)) + '</a>' : '';
|
||||
},
|
||||
|
||||
fixUrl: function(url) {
|
||||
return url.indexOf(':') < 0 ? 'http://' + url : url;
|
||||
},
|
||||
|
||||
displayUrl: function(url) {
|
||||
return url.replace(this.displayUrlRegex, '');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -5,16 +5,13 @@ var Backbone = require('backbone'),
|
|||
CopyPaste = require('../../comp/copy-paste');
|
||||
|
||||
var FieldView = Backbone.View.extend({
|
||||
template: require('templates/details/field.html'),
|
||||
template: require('templates/details/field.hbs'),
|
||||
|
||||
events: {
|
||||
'click .details__field-label': 'fieldLabelClick',
|
||||
'click .details__field-value': 'fieldValueClick'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.value = typeof this.model.value === 'function' ? this.model.value() : this.model.value;
|
||||
this.renderTemplate({ editable: !this.readonly, multiline: this.model.multiline, title: this.model.title,
|
||||
|
@ -39,7 +36,7 @@ var FieldView = Backbone.View.extend({
|
|||
var field = this.model.name;
|
||||
if (FeatureDetector.shouldMoveHiddenInputToCopySource()) {
|
||||
var box = this.valueEl[0].getBoundingClientRect();
|
||||
var textValue = this.value && this.value.getText ? this.value.getText() : this.getEditValue(this.value);
|
||||
var textValue = this.value && this.value.isProtected ? this.value.getText() : this.getEditValue(this.value);
|
||||
if (!textValue) {
|
||||
return;
|
||||
}
|
||||
|
@ -49,7 +46,7 @@ var FieldView = Backbone.View.extend({
|
|||
}
|
||||
if (field) {
|
||||
var value = this.value || '';
|
||||
if (value && value.getText) {
|
||||
if (value && value.isProtected) {
|
||||
CopyPaste.createHiddenInput(value.getText());
|
||||
CopyPaste.tryCopy();
|
||||
return;
|
||||
|
@ -89,12 +86,18 @@ var FieldView = Backbone.View.extend({
|
|||
return;
|
||||
}
|
||||
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 textEqual;
|
||||
if (this.value && this.value.isProtected) {
|
||||
textEqual = this.value.equals(newVal);
|
||||
} else if (newVal && newVal.isProtected) {
|
||||
textEqual = newVal.equals(this.value);
|
||||
} else {
|
||||
textEqual = _.isEqual(this.value, newVal);
|
||||
}
|
||||
var protectedEqual = (newVal && newVal.isProtected) === (this.value && this.value.isProtected);
|
||||
var nameChanged = extra && extra.newField;
|
||||
var arg;
|
||||
if (newVal !== undefined && (!textEqual || !protectedEqual)) {
|
||||
if (newVal !== undefined && (!textEqual || !protectedEqual || nameChanged)) {
|
||||
arg = { val: newVal, field: this.model.name };
|
||||
if (extra) {
|
||||
_.extend(arg, extra);
|
||||
|
|
|
@ -4,10 +4,11 @@ var Backbone = require('backbone'),
|
|||
Keys = require('../const/keys'),
|
||||
KeyHandler = require('../comp/key-handler'),
|
||||
GeneratorView = require('./generator-view'),
|
||||
Tip = require('../util/tip'),
|
||||
UpdateModel = require('../models/update-model');
|
||||
|
||||
var FooterView = Backbone.View.extend({
|
||||
template: require('templates/footer.html'),
|
||||
template: require('templates/footer.hbs'),
|
||||
|
||||
events: {
|
||||
'click .footer__db-item': 'showFile',
|
||||
|
@ -36,6 +37,7 @@ var FooterView = Backbone.View.extend({
|
|||
files: this.model.files,
|
||||
updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
|
||||
}));
|
||||
Tip.createTips(this.$el);
|
||||
return this;
|
||||
},
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
var Backbone = require('backbone'),
|
||||
PasswordGenerator = require('../util/password-generator'),
|
||||
CopyPaste = require('../comp/copy-paste');
|
||||
CopyPaste = require('../comp/copy-paste'),
|
||||
Locale = require('../util/locale');
|
||||
|
||||
var DefaultGenOpts = {
|
||||
length: 16, upper: true, lower: true, digits: true, special: false, brackets: false, high: false, ambiguous: false
|
||||
|
@ -11,7 +12,7 @@ var DefaultGenOpts = {
|
|||
var GeneratorView = Backbone.View.extend({
|
||||
el: 'body',
|
||||
|
||||
template: require('templates/generator.html'),
|
||||
template: require('templates/generator.hbs'),
|
||||
|
||||
events: {
|
||||
'click': 'click',
|
||||
|
@ -31,7 +32,7 @@ var GeneratorView = Backbone.View.extend({
|
|||
|
||||
render: function() {
|
||||
var canCopy = document.queryCommandSupported('copy');
|
||||
var btnTitle = this.model.copy ? canCopy ? 'Copy' : 'Close' : 'OK';
|
||||
var btnTitle = this.model.copy ? canCopy ? Locale.alertCopy : Locale.alertClose : Locale.alertOk;
|
||||
this.renderTemplate({ btnTitle: btnTitle, opt: this.gen });
|
||||
this.resultEl = this.$el.find('.gen__result');
|
||||
this.$el.css(this.model.pos);
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
var Backbone = require('backbone'),
|
||||
Scrollable = require('../mixins/scrollable'),
|
||||
IconSelectView = require('./icon-select-view'),
|
||||
baron = require('baron');
|
||||
Tip = require('../util/tip');
|
||||
|
||||
var GrpView = Backbone.View.extend({
|
||||
template: require('templates/grp.html'),
|
||||
template: require('templates/grp.hbs'),
|
||||
|
||||
events: {
|
||||
'click .grp__icon': 'showIconsSelect',
|
||||
|
@ -30,19 +30,16 @@ var GrpView = Backbone.View.extend({
|
|||
enableSearching: this.model.get('enableSearching') !== false,
|
||||
readonly: this.model.get('top')
|
||||
}));
|
||||
Tip.createTips(this.$el);
|
||||
if (!this.model.get('title')) {
|
||||
this.$el.find('#grp__field-title').focus();
|
||||
}
|
||||
}
|
||||
this.scroll = baron({
|
||||
this.createScroll({
|
||||
root: this.$el.find('.details__body')[0],
|
||||
scroller: this.$el.find('.scroller')[0],
|
||||
bar: this.$el.find('.scroller__bar')[0],
|
||||
$: Backbone.$
|
||||
bar: this.$el.find('.scroller__bar')[0]
|
||||
});
|
||||
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;
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@ var Backbone = require('backbone'),
|
|||
var logger = new Logger('icon-select-view');
|
||||
|
||||
var IconSelectView = Backbone.View.extend({
|
||||
template: require('templates/icon-select.html'),
|
||||
template: require('templates/icon-select.hbs'),
|
||||
|
||||
events: {
|
||||
'click .icon-select__icon': 'iconClick',
|
||||
|
@ -77,16 +77,16 @@ var IconSelectView = Backbone.View.extend({
|
|||
};
|
||||
},
|
||||
|
||||
getIconUrl: function(useGoogle) {
|
||||
getIconUrl: function(useService) {
|
||||
if (!this.model.url) {
|
||||
return null;
|
||||
}
|
||||
var url = this.model.url.replace(/([^\/:]\/.*)?$/, function(match) { return (match && match[0]) + '/favicon.ico'; });
|
||||
if (url.indexOf('://') < 0) {
|
||||
if (url.indexOf('://') >= 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
if (useGoogle) {
|
||||
return 'http://www.google.com/s2/favicons?domain_url=' + encodeURIComponent(url.replace('/favicon.ico', '/'));
|
||||
if (useService) {
|
||||
return 'https://favicon-antelle.rhcloud.com/' + url.replace(/^.*:\/+/, '').replace(/\/.*/, '');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone'),
|
||||
SecureInput = require('../comp/secure-input'),
|
||||
Alerts = require('../comp/alerts'),
|
||||
Locale = require('../util/locale'),
|
||||
Keys = require('../const/keys');
|
||||
|
||||
var KeyChangeView = Backbone.View.extend({
|
||||
template: require('templates/key-change.hbs'),
|
||||
|
||||
events: {
|
||||
'keydown .key-change__pass': 'inputKeydown',
|
||||
'click .key-change__keyfile': 'keyFileClicked',
|
||||
'change .key-change__file': 'keyFileSelected',
|
||||
'click .key-change__btn-ok': 'accept',
|
||||
'click .key-change__btn-cancel': 'cancel'
|
||||
},
|
||||
|
||||
passwordInput: null,
|
||||
inputEl: null,
|
||||
|
||||
initialize: function() {
|
||||
this.passwordInput = new SecureInput();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.keyFileName = this.model.get('keyFileName') || null;
|
||||
this.keyFileData = null;
|
||||
this.renderTemplate({
|
||||
fileName: this.model.get('name'),
|
||||
keyFileName: this.model.get('keyFileName')
|
||||
});
|
||||
this.$el.find('.key-change__keyfile-name').text(this.keyFileName ? ': ' + this.keyFileName : '');
|
||||
this.inputEl = this.$el.find('.key-change__pass');
|
||||
this.passwordInput.reset();
|
||||
this.passwordInput.setElement(this.inputEl);
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
Backbone.View.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
inputKeydown: function(e) {
|
||||
var code = e.keyCode || e.which;
|
||||
if (code === Keys.DOM_VK_RETURN) {
|
||||
this.accept();
|
||||
} else if (code === Keys.DOM_VK_A) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
|
||||
keyFileClicked: function() {
|
||||
if (this.keyFileName) {
|
||||
this.keyFileName = null;
|
||||
this.keyFile = null;
|
||||
this.$el.find('.key-change__keyfile-name').html('');
|
||||
}
|
||||
this.$el.find('.key-change__file').val(null).click();
|
||||
this.inputEl.focus();
|
||||
},
|
||||
|
||||
keyFileSelected: function(e) {
|
||||
var file = e.target.files[0];
|
||||
if (file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = (function(e) {
|
||||
this.keyFileName = file.name;
|
||||
this.keyFileData = e.target.result;
|
||||
this.$el.find('.key-change__keyfile-name').text(': ' + this.keyFileName);
|
||||
}).bind(this);
|
||||
reader.onerror = function() {
|
||||
Alerts.error({ header: Locale.openFailedRead });
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
} else {
|
||||
this.$el.find('.key-change__keyfile-name').html('');
|
||||
}
|
||||
this.inputEl.focus();
|
||||
},
|
||||
|
||||
accept: function() {
|
||||
this.trigger('accept', {
|
||||
file: this.model,
|
||||
password: this.passwordInput.value,
|
||||
keyFileName: this.keyFileName,
|
||||
keyFileData: this.keyFileData
|
||||
});
|
||||
},
|
||||
|
||||
cancel: function() {
|
||||
this.trigger('cancel');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = KeyChangeView;
|
|
@ -4,10 +4,11 @@ var Backbone = require('backbone'),
|
|||
Keys = require('../const/keys'),
|
||||
KeyHandler = require('../comp/key-handler'),
|
||||
DropdownView = require('./dropdown-view'),
|
||||
FeatureDetector = require('../util/feature-detector');
|
||||
FeatureDetector = require('../util/feature-detector'),
|
||||
Locale = require('../util/locale');
|
||||
|
||||
var ListSearchView = Backbone.View.extend({
|
||||
template: require('templates/list-search.html'),
|
||||
template: require('templates/list-search.hbs'),
|
||||
|
||||
events: {
|
||||
'keydown .list__search-field': 'inputKeyDown',
|
||||
|
@ -16,7 +17,8 @@ var ListSearchView = Backbone.View.extend({
|
|||
'click .list__search-btn-new': 'createOptionsClick',
|
||||
'click .list__search-btn-sort': 'sortOptionsClick',
|
||||
'click .list__search-icon-search': 'advancedSearchClick',
|
||||
'click .list__search-btn-menu': 'toggleMenu'
|
||||
'click .list__search-btn-menu': 'toggleMenu',
|
||||
'change .list__search-adv input[type=checkbox]': 'toggleAdvCheck'
|
||||
},
|
||||
|
||||
views: null,
|
||||
|
@ -25,31 +27,41 @@ var ListSearchView = Backbone.View.extend({
|
|||
sortOptions: null,
|
||||
sortIcons: null,
|
||||
createOptions: null,
|
||||
advancedSearchEnabled: false,
|
||||
advancedSearch: null,
|
||||
|
||||
initialize: function () {
|
||||
this.sortOptions = [
|
||||
{ value: 'title', icon: 'sort-alpha-asc', text: 'Title A → Z' },
|
||||
{ value: '-title', icon: 'sort-alpha-desc', text: 'Title Z → A' },
|
||||
{ value: 'website', icon: 'sort-alpha-asc', text: 'Website A → Z' },
|
||||
{ value: '-website', icon: 'sort-alpha-desc', text: 'Website Z → A' },
|
||||
{ value: 'user', icon: 'sort-alpha-asc', text: 'User A → Z' },
|
||||
{ value: '-user', icon: 'sort-alpha-desc', text: 'User Z → A' },
|
||||
{ value: 'created', icon: 'sort-numeric-asc', text: 'Created Old → New' },
|
||||
{ value: '-created', icon: 'sort-numeric-desc', text: 'Created New → Old' },
|
||||
{ value: 'updated', icon: 'sort-numeric-asc', text: 'Updated Old → New' },
|
||||
{ value: '-updated', icon: 'sort-numeric-desc', text: 'Updated New → Old' },
|
||||
{ value: '-attachments', icon: 'sort-amount-desc', text: 'Attachments' }
|
||||
{ value: 'title', icon: 'sort-alpha-asc', text: Locale.searchTitle + ' ' + Locale.searchAZ },
|
||||
{ value: '-title', icon: 'sort-alpha-desc', text: Locale.searchTitle + ' ' + Locale.searchZA },
|
||||
{ value: 'website', icon: 'sort-alpha-asc', text: Locale.searchWebsite + ' ' + Locale.searchAZ },
|
||||
{ value: '-website', icon: 'sort-alpha-desc', text: Locale.searchWebsite + ' ' + Locale.searchZA },
|
||||
{ value: 'user', icon: 'sort-alpha-asc', text: Locale.searchUser + ' ' + Locale.searchAZ },
|
||||
{ value: '-user', icon: 'sort-alpha-desc', text: Locale.searchUser + ' ' + Locale.searchZA },
|
||||
{ value: 'created', icon: 'sort-numeric-asc', text: Locale.searchCreated + ' ' + Locale.searchON },
|
||||
{ value: '-created', icon: 'sort-numeric-desc', text: Locale.searchCreated + ' ' + Locale.searchNO },
|
||||
{ value: 'updated', icon: 'sort-numeric-asc', text: Locale.searchUpdated + ' ' + Locale.searchON },
|
||||
{ value: '-updated', icon: 'sort-numeric-desc', text: Locale.searchUpdated + ' ' + Locale.searchNO },
|
||||
{ value: '-attachments', icon: 'sort-amount-desc', text: Locale.searchAttachments }
|
||||
];
|
||||
this.sortIcons = {};
|
||||
this.sortOptions.forEach(function(opt) {
|
||||
this.sortIcons[opt.value] = opt.icon;
|
||||
}, this);
|
||||
var entryDesc = FeatureDetector.isMobile() ? '' : (' <span class="muted-color">(' + Locale.searchShiftClickOr + ' ' +
|
||||
FeatureDetector.altShortcutSymbol(true) + 'N)</span>');
|
||||
this.createOptions = [
|
||||
{ value: 'entry', icon: 'key', text: 'Entry <span class="muted-color">(shift-click or ' +
|
||||
FeatureDetector.altShortcutSymbol(true) + 'N)</span>' },
|
||||
{ value: 'entry', icon: 'key', text: 'Entry' + entryDesc },
|
||||
{ value: 'group', icon: 'folder', text: 'Group' }
|
||||
];
|
||||
this.views = {};
|
||||
this.advancedSearch = {
|
||||
user: true, other: true,
|
||||
url: true, protect: false,
|
||||
notes: true, pass: false,
|
||||
cs: false, regex: false,
|
||||
history: false
|
||||
};
|
||||
KeyHandler.onKey(Keys.DOM_VK_F, this.findKeyPress, this, KeyHandler.SHORTCUT_ACTION);
|
||||
KeyHandler.onKey(Keys.DOM_VK_N, this.newKeyPress, this, KeyHandler.SHORTCUT_OPT);
|
||||
KeyHandler.onKey(Keys.DOM_VK_DOWN, this.downKeyPress, this);
|
||||
|
@ -76,7 +88,7 @@ var ListSearchView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
render: function () {
|
||||
this.renderTemplate();
|
||||
this.renderTemplate({ adv: this.advancedSearch });
|
||||
this.inputEl = this.$el.find('.list__search-field');
|
||||
return this;
|
||||
},
|
||||
|
@ -96,6 +108,12 @@ var ListSearchView = Backbone.View.extend({
|
|||
}
|
||||
e.target.blur();
|
||||
break;
|
||||
case Keys.DOM_VK_A:
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
@ -160,6 +178,11 @@ var ListSearchView = Backbone.View.extend({
|
|||
}
|
||||
var sortIconCls = this.sortIcons[filter.sort] || 'sort';
|
||||
this.$el.find('.list__search-btn-sort>i').attr('class', 'fa fa-' + sortIconCls);
|
||||
var adv = !!filter.filter.advanced;
|
||||
if (this.advancedSearchEnabled !== adv) {
|
||||
this.advancedSearchEnabled = adv;
|
||||
this.$el.find('.list__search-adv').toggleClass('hide', !this.advancedSearchEnabled);
|
||||
}
|
||||
},
|
||||
|
||||
createOptionsClick: function(e) {
|
||||
|
@ -178,13 +201,21 @@ var ListSearchView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
advancedSearchClick: function() {
|
||||
require('../comp/alerts').notImplemented();
|
||||
this.advancedSearchEnabled = !this.advancedSearchEnabled;
|
||||
this.$el.find('.list__search-adv').toggleClass('hide', !this.advancedSearchEnabled);
|
||||
Backbone.trigger('add-filter', { advanced: this.advancedSearchEnabled ? this.advancedSearch : false });
|
||||
},
|
||||
|
||||
toggleMenu: function() {
|
||||
Backbone.trigger('toggle-menu');
|
||||
},
|
||||
|
||||
toggleAdvCheck: function(e) {
|
||||
var setting = $(e.target).data('id');
|
||||
this.advancedSearch[setting] = e.target.checked;
|
||||
Backbone.trigger('add-filter', { advanced: this.advancedSearch });
|
||||
},
|
||||
|
||||
hideSearchOptions: function() {
|
||||
if (this.views.searchDropdown) {
|
||||
this.views.searchDropdown.remove();
|
||||
|
|
|
@ -6,12 +6,11 @@ var Backbone = require('backbone'),
|
|||
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');
|
||||
AppSettingsModel = require('../models/app-settings-model');
|
||||
|
||||
var ListView = Backbone.View.extend({
|
||||
template: require('templates/list.html'),
|
||||
emptyTemplate: require('templates/list-empty.html'),
|
||||
template: require('templates/list.hbs'),
|
||||
emptyTemplate: require('templates/list-empty.hbs'),
|
||||
|
||||
events: {
|
||||
'click .list__item': 'itemClick',
|
||||
|
@ -54,14 +53,11 @@ var ListView = Backbone.View.extend({
|
|||
this.views.search.setElement(this.$el.find('.list__header')).render();
|
||||
this.setTableView();
|
||||
|
||||
this.scroll = baron({
|
||||
this.createScroll({
|
||||
root: this.$el.find('.list__items')[0],
|
||||
scroller: this.$el.find('.scroller')[0],
|
||||
bar: this.$el.find('.scroller__bar')[0],
|
||||
$: Backbone.$
|
||||
bar: this.$el.find('.scroller__bar')[0]
|
||||
});
|
||||
this.scrollerBar = this.$el.find('.scroller__bar');
|
||||
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
|
||||
}
|
||||
if (this.items.length) {
|
||||
var itemTemplate = this.getItemTemplate();
|
||||
|
@ -84,7 +80,7 @@ var ListView = Backbone.View.extend({
|
|||
|
||||
getItemsTemplate: function() {
|
||||
if (this.model.settings.get('tableView')) {
|
||||
return require('templates/list-table.html');
|
||||
return require('templates/list-table.hbs');
|
||||
} else {
|
||||
return this.renderPlainItems;
|
||||
}
|
||||
|
@ -96,9 +92,9 @@ var ListView = Backbone.View.extend({
|
|||
|
||||
getItemTemplate: function() {
|
||||
if (this.model.settings.get('tableView')) {
|
||||
return require('templates/list-item-table.html');
|
||||
return require('templates/list-item-table.hbs');
|
||||
} else {
|
||||
return require('templates/list-item-short.html');
|
||||
return require('templates/list-item-short.hbs');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -4,10 +4,11 @@ var Backbone = require('backbone'),
|
|||
KeyHandler = require('../../comp/key-handler'),
|
||||
Keys = require('../../const/keys'),
|
||||
Alerts = require('../../comp/alerts'),
|
||||
DragDropInfo = require('../../comp/drag-drop-info');
|
||||
DragDropInfo = require('../../comp/drag-drop-info'),
|
||||
Locale = require('../../util/locale');
|
||||
|
||||
var MenuItemView = Backbone.View.extend({
|
||||
template: require('templates/menu/menu-item.html'),
|
||||
template: require('templates/menu/menu-item.hbs'),
|
||||
|
||||
events: {
|
||||
'mouseover': 'mouseover',
|
||||
|
@ -162,8 +163,8 @@ var MenuItemView = Backbone.View.extend({
|
|||
emptyTrash: function(e) {
|
||||
e.stopPropagation();
|
||||
Alerts.yesno({
|
||||
header: 'Empty trash?',
|
||||
body: 'You will not be able to put items back',
|
||||
header: Locale.menuEmptyTrashAlert,
|
||||
body: Locale.menuEmptyTrashAlertBody,
|
||||
icon: 'minus-circle',
|
||||
success: function() {
|
||||
Backbone.trigger('empty-trash');
|
||||
|
|
|
@ -4,11 +4,10 @@ var Backbone = require('backbone'),
|
|||
MenuItemView = require('./menu-item-view'),
|
||||
Resizable = require('../../mixins/resizable'),
|
||||
Scrollable = require('../../mixins/scrollable'),
|
||||
AppSettingsModel = require('../../models/app-settings-model'),
|
||||
baron = require('baron');
|
||||
AppSettingsModel = require('../../models/app-settings-model');
|
||||
|
||||
var MenuSectionView = Backbone.View.extend({
|
||||
template: require('templates/menu/menu-section.html'),
|
||||
template: require('templates/menu/menu-section.hbs'),
|
||||
|
||||
events: {},
|
||||
|
||||
|
@ -30,14 +29,11 @@ var MenuSectionView = Backbone.View.extend({
|
|||
this.itemsEl = this.model.get('scrollable') ? this.$el.find('.scroller') : this.$el;
|
||||
if (this.model.get('scrollable')) {
|
||||
this.initScroll();
|
||||
this.scroll = baron({
|
||||
this.createScroll({
|
||||
root: this.$el[0],
|
||||
scroller: this.$el.find('.scroller')[0],
|
||||
bar: this.$el.find('.scroller__bar')[0],
|
||||
$: Backbone.$
|
||||
bar: this.$el.find('.scroller__bar')[0]
|
||||
});
|
||||
this.scrollerBar = this.$el.find('.scroller__bar');
|
||||
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
|
||||
}
|
||||
} else {
|
||||
this.removeInnerViews();
|
||||
|
|
|
@ -7,7 +7,7 @@ var Backbone = require('backbone'),
|
|||
AppSettingsModel = require('../../models/app-settings-model');
|
||||
|
||||
var MenuView = Backbone.View.extend({
|
||||
template: require('templates/menu/menu.html'),
|
||||
template: require('templates/menu/menu.hbs'),
|
||||
|
||||
events: {},
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ var Backbone = require('backbone'),
|
|||
var ModalView = Backbone.View.extend({
|
||||
el: 'body',
|
||||
|
||||
template: require('templates/modal.html'),
|
||||
template: require('templates/modal.hbs'),
|
||||
|
||||
events: {
|
||||
'click .modal__buttons button': 'buttonClick',
|
||||
|
|
|
@ -5,12 +5,13 @@ var Backbone = require('backbone'),
|
|||
Alerts = require('../comp/alerts'),
|
||||
SecureInput = require('../comp/secure-input'),
|
||||
DropboxLink = require('../comp/dropbox-link'),
|
||||
Logger = require('../util/logger');
|
||||
Logger = require('../util/logger'),
|
||||
Locale = require('../util/locale');
|
||||
|
||||
var logger = new Logger('open-view');
|
||||
|
||||
var OpenView = Backbone.View.extend({
|
||||
template: require('templates/open.html'),
|
||||
template: require('templates/open.hbs'),
|
||||
|
||||
events: {
|
||||
'change .open__file-ctrl': 'fileSelected',
|
||||
|
@ -113,7 +114,7 @@ var OpenView = Backbone.View.extend({
|
|||
}
|
||||
}).bind(this);
|
||||
reader.onerror = (function() {
|
||||
Alerts.error({ header: 'Failed to read file' });
|
||||
Alerts.error({ header: Locale.openFailedRead });
|
||||
if (complete) {
|
||||
complete(false);
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ var OpenView = Backbone.View.extend({
|
|||
this.$el.addClass('open--file');
|
||||
this.$el.find('.open__settings-key-file').removeClass('hide');
|
||||
this.inputEl[0].removeAttribute('readonly');
|
||||
this.inputEl[0].setAttribute('placeholder', 'Password for ' + this.params.name);
|
||||
this.inputEl[0].setAttribute('placeholder', Locale.openPassFor + ' ' + this.params.name);
|
||||
this.inputEl.focus();
|
||||
},
|
||||
|
||||
|
@ -290,16 +291,15 @@ var OpenView = Backbone.View.extend({
|
|||
});
|
||||
if (!buttons.length) {
|
||||
Alerts.error({
|
||||
header: 'Nothing found',
|
||||
body: 'You have no files in your Dropbox which could be opened.' +
|
||||
(dirStat && dirStat.inAppFolder ? ' Files are searched inside app folder in your Dropbox.' : '')
|
||||
header: Locale.openNothingFound,
|
||||
body: Locale.openNothingFoundBody + (dirStat && dirStat.inAppFolder ? ' ' + Locale.openNothingFoundBodyAppFolder : '')
|
||||
});
|
||||
return;
|
||||
}
|
||||
buttons.push({ result: '', title: 'Cancel' });
|
||||
buttons.push({ result: '', title: Locale.alertCancel });
|
||||
Alerts.alert({
|
||||
header: 'Select a file',
|
||||
body: 'Select a file from your Dropbox which you would like to open',
|
||||
header: Locale.openSelectFile,
|
||||
body: Locale.openSelectFileBody,
|
||||
icon: 'dropbox',
|
||||
buttons: buttons,
|
||||
esc: '',
|
||||
|
|
|
@ -5,7 +5,7 @@ var Backbone = require('backbone'),
|
|||
Links = require('../../const/links');
|
||||
|
||||
var SettingsAboutView = Backbone.View.extend({
|
||||
template: require('templates/settings/settings-about.html'),
|
||||
template: require('templates/settings/settings-about.hbs'),
|
||||
|
||||
render: function() {
|
||||
this.renderTemplate({
|
||||
|
|
|
@ -9,11 +9,12 @@ var Backbone = require('backbone'),
|
|||
Links = require('../../const/links'),
|
||||
DropboxLink = require('../../comp/dropbox-link'),
|
||||
Format = require('../../util/format'),
|
||||
Locale = require('../../util/locale'),
|
||||
kdbxweb = require('kdbxweb'),
|
||||
FileSaver = require('filesaver');
|
||||
|
||||
var SettingsAboutView = Backbone.View.extend({
|
||||
template: require('templates/settings/settings-file.html'),
|
||||
template: require('templates/settings/settings-file.hbs'),
|
||||
|
||||
events: {
|
||||
'click .settings__file-button-save-default': 'saveDefault',
|
||||
|
@ -72,14 +73,15 @@ var SettingsAboutView = Backbone.View.extend({
|
|||
var sel = this.$el.find('#settings__file-key-file');
|
||||
sel.html('');
|
||||
if (keyFileName && keyFileChanged) {
|
||||
var text = keyFileName !== 'Generated' ? 'Use key file ' + keyFileName : 'Use generated key file';
|
||||
var text = keyFileName !== 'Generated' ? Locale.setFileUseKeyFile + ' ' + keyFileName : Locale.setFileUseGenKeyFile;
|
||||
$('<option/>').val('ex').text(text).appendTo(sel);
|
||||
}
|
||||
if (oldKeyFileName) {
|
||||
$('<option/>').val('old').text('Use ' + (keyFileChanged ? 'old ' : '') + 'key file ' + oldKeyFileName).appendTo(sel);
|
||||
var useText = keyFileChanged ? Locale.setFileUseOldKeyFile : Locale.setFileUseKeyFile + ' ' + oldKeyFileName;
|
||||
$('<option/>').val('old').text(useText).appendTo(sel);
|
||||
}
|
||||
$('<option/>').val('gen').text('Generate new key file').appendTo(sel);
|
||||
$('<option/>').val('none').text('Don\'t use key file').appendTo(sel);
|
||||
$('<option/>').val('gen').text(Locale.setFileGenKeyFile).appendTo(sel);
|
||||
$('<option/>').val('none').text(Locale.setFileDontUseKeyFile).appendTo(sel);
|
||||
if (keyFileName && keyFileChanged) {
|
||||
sel.val('ex');
|
||||
} else if (!keyFileName) {
|
||||
|
@ -93,8 +95,8 @@ var SettingsAboutView = Backbone.View.extend({
|
|||
if (!this.model.get('passwordLength')) {
|
||||
var that = this;
|
||||
Alerts.yesno({
|
||||
header: 'Empty password',
|
||||
body: 'Saving database with empty password makes it completely unprotected. Do you really want to do it?',
|
||||
header: Locale.setFileEmptyPass,
|
||||
body: Locale.setFileEmptyPassBody,
|
||||
success: function() {
|
||||
continueCallback();
|
||||
},
|
||||
|
@ -149,8 +151,8 @@ var SettingsAboutView = Backbone.View.extend({
|
|||
Storage.file.save(path, data, function (err) {
|
||||
if (err) {
|
||||
Alerts.error({
|
||||
header: 'Save error',
|
||||
body: 'Error saving to file ' + path + ': \n' + err
|
||||
header: Locale.setFileSaveError,
|
||||
body: Locale.setFileSaveErrorBody + ' ' + path + ': \n' + err
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -191,8 +193,8 @@ var SettingsAboutView = Backbone.View.extend({
|
|||
if (existingPath) {
|
||||
Alerts.yesno({
|
||||
icon: 'dropbox',
|
||||
header: 'Already exists',
|
||||
body: 'File ' + that.model.escape('name') + ' already exists in your Dropbox. Overwrite it?',
|
||||
header: Locale.setFileAlreadyExists,
|
||||
body: Locale.setFileAlreadyExistsBody.replace('{}', that.model.escape('name')),
|
||||
success: function() {
|
||||
that.model.set('syncing', true);
|
||||
DropboxLink.deleteFile(existingPath, function(err) {
|
||||
|
@ -215,12 +217,11 @@ var SettingsAboutView = Backbone.View.extend({
|
|||
if (this.model.get('modified')) {
|
||||
var that = this;
|
||||
Alerts.yesno({
|
||||
header: 'Unsaved changes',
|
||||
body: 'There are unsaved changes in this file',
|
||||
header: Locale.setFileUnsaved,
|
||||
body: Locale.setFileUnsavedBody,
|
||||
buttons: [
|
||||
//{result: 'save', title: 'Save and close'},
|
||||
{result: 'close', title: 'Close and lose changes', error: true},
|
||||
{result: '', title: 'Don\t close'}
|
||||
{result: 'close', title: Locale.setFileCloseNoSave, error: true},
|
||||
{result: '', title: Locale.setFileDontClose}
|
||||
],
|
||||
success: function(result) {
|
||||
if (result === 'close') {
|
||||
|
|
|
@ -8,10 +8,11 @@ var Backbone = require('backbone'),
|
|||
UpdateModel = require('../../models/update-model'),
|
||||
RuntimeInfo = require('../../comp/runtime-info'),
|
||||
FeatureDetector = require('../../util/feature-detector'),
|
||||
Locale = require('../../util/locale'),
|
||||
Links = require('../../const/links');
|
||||
|
||||
var SettingsGeneralView = Backbone.View.extend({
|
||||
template: require('templates/settings/settings-general.html'),
|
||||
template: require('templates/settings/settings-general.hbs'),
|
||||
|
||||
events: {
|
||||
'change .settings__general-theme': 'changeTheme',
|
||||
|
@ -43,6 +44,9 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var updateReady = UpdateModel.instance.get('updateStatus') === 'ready',
|
||||
updateFound = UpdateModel.instance.get('updateStatus') === 'found',
|
||||
updateManual = UpdateModel.instance.get('updateManual');
|
||||
this.renderTemplate({
|
||||
themes: this.allThemes,
|
||||
activeTheme: AppSettingsModel.instance.get('theme'),
|
||||
|
@ -61,9 +65,11 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
autoUpdate: Updater.getAutoUpdateType(),
|
||||
updateInProgress: Updater.updateInProgress(),
|
||||
updateInfo: this.getUpdateInfo(),
|
||||
updateReady: UpdateModel.instance.get('updateStatus') === 'ready',
|
||||
updateFound: UpdateModel.instance.get('updateStatus') === 'found',
|
||||
updateManual: UpdateModel.instance.get('updateManual'),
|
||||
updateWaitingReload: updateReady && !Launcher,
|
||||
showUpdateBlock: Launcher && !updateManual,
|
||||
updateReady: updateReady,
|
||||
updateFound: updateFound,
|
||||
updateManual: updateManual,
|
||||
releaseNotesLink: Links.ReleaseNotes,
|
||||
colorfulIcons: AppSettingsModel.instance.get('colorfulIcons')
|
||||
});
|
||||
|
@ -72,36 +78,36 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
getUpdateInfo: function() {
|
||||
switch (UpdateModel.instance.get('status')) {
|
||||
case 'checking':
|
||||
return 'Checking for updates...';
|
||||
return Locale.setGenUpdateChecking + '...';
|
||||
case 'error':
|
||||
var errMsg = 'Error checking for updates';
|
||||
var errMsg = Locale.setGenErrorChecking;
|
||||
if (UpdateModel.instance.get('lastError')) {
|
||||
errMsg += ': ' + UpdateModel.instance.get('lastError');
|
||||
}
|
||||
if (UpdateModel.instance.get('lastSuccessCheckDate')) {
|
||||
errMsg += '. Last successful check was at ' + Format.dtStr(UpdateModel.instance.get('lastSuccessCheckDate')) +
|
||||
': the latest version was ' + UpdateModel.instance.get('lastVersion');
|
||||
errMsg += '. ' + Locale.setGenLastCheckSuccess.replace('{}', Format.dtStr(UpdateModel.instance.get('lastSuccessCheckDate'))) +
|
||||
': ' + Locale.setGenLastCheckVer.replace('{}', UpdateModel.instance.get('lastVersion'));
|
||||
}
|
||||
return errMsg;
|
||||
case 'ok':
|
||||
var msg = 'Checked at ' + Format.dtStr(UpdateModel.instance.get('lastCheckDate')) + ': ';
|
||||
var msg = Locale.setGenCheckedAt + ' ' + Format.dtStr(UpdateModel.instance.get('lastCheckDate')) + ': ';
|
||||
if (RuntimeInfo.version === UpdateModel.instance.get('lastVersion')) {
|
||||
msg += 'you are using the latest version';
|
||||
msg += Locale.setGenLatestVer;
|
||||
} else {
|
||||
msg += 'new version ' + UpdateModel.instance.get('lastVersion') + ' available, released at ' +
|
||||
msg += Locale.setGenNewVer.replace('{}', UpdateModel.instance.get('lastVersion')) + ' ' +
|
||||
Format.dStr(UpdateModel.instance.get('lastVersionReleaseDate'));
|
||||
}
|
||||
switch (UpdateModel.instance.get('updateStatus')) {
|
||||
case 'downloading':
|
||||
return msg + '. Downloading update...';
|
||||
return msg + '. ' + Locale.setGenDownloadingUpdate;
|
||||
case 'extracting':
|
||||
return msg + '. Extracting update...';
|
||||
return msg + '. ' + Locale.setGenExtractingUpdate;
|
||||
case 'error':
|
||||
return msg + '. There was an error downloading new version';
|
||||
return msg + '. ' + Locale.setGenCheckErr;
|
||||
}
|
||||
return msg;
|
||||
default:
|
||||
return 'Never checked for updates';
|
||||
return Locale.setGenNeverChecked;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ var Backbone = require('backbone'),
|
|||
Links = require('../../const/links');
|
||||
|
||||
var SettingsHelpView = Backbone.View.extend({
|
||||
template: require('templates/settings/settings-help.html'),
|
||||
template: require('templates/settings/settings-help.hbs'),
|
||||
|
||||
render: function() {
|
||||
var appInfo = 'KeeWeb v' + RuntimeInfo.version + ' (' + RuntimeInfo.commit + ', ' + RuntimeInfo.buildDate + ')\n' +
|
||||
|
|
|
@ -4,7 +4,7 @@ var Backbone = require('backbone'),
|
|||
FeatureDetector = require('../../util/feature-detector');
|
||||
|
||||
var SettingsShortcutsView = Backbone.View.extend({
|
||||
template: require('templates/settings/settings-shortcuts.html'),
|
||||
template: require('templates/settings/settings-shortcuts.hbs'),
|
||||
|
||||
render: function() {
|
||||
this.renderTemplate({
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
var Backbone = require('backbone'),
|
||||
Scrollable = require('../../mixins/scrollable'),
|
||||
Keys = require('../../const/keys'),
|
||||
KeyHandler = require('../../comp/key-handler'),
|
||||
baron = require('baron');
|
||||
KeyHandler = require('../../comp/key-handler');
|
||||
|
||||
var SettingsView = Backbone.View.extend({
|
||||
template: require('templates/settings/settings.html'),
|
||||
template: require('templates/settings/settings.hbs'),
|
||||
|
||||
views: null,
|
||||
|
||||
|
@ -28,14 +27,11 @@ var SettingsView = Backbone.View.extend({
|
|||
|
||||
render: function () {
|
||||
this.renderTemplate();
|
||||
this.scroll = baron({
|
||||
this.createScroll({
|
||||
root: this.$el.find('.settings')[0],
|
||||
scroller: this.$el.find('.scroller')[0],
|
||||
bar: this.$el.find('.scroller__bar')[0],
|
||||
$: Backbone.$
|
||||
bar: this.$el.find('.scroller__bar')[0]
|
||||
});
|
||||
this.scrollerBar = this.$el.find('.scroller__bar');
|
||||
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
|
||||
this.pageEl = this.$el.find('.scroller');
|
||||
return this;
|
||||
},
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
@include align-items(stretch);
|
||||
@include flex-direction(row);
|
||||
@include justify-content(flex-start);
|
||||
overflow: hidden;
|
||||
&.app__list-wrap--table {
|
||||
@include flex-direction(column);
|
||||
}
|
||||
|
|
|
@ -134,10 +134,12 @@
|
|||
@include scrollbar-full-width-hack();
|
||||
@-moz-document url-prefix() { @include scrollbar-padding-hack(); }
|
||||
@at-root { _:-ms-lang(x), .details__body>.scroller { @include scrollbar-padding-hack(); } }
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) { width: 100% !important; }
|
||||
}
|
||||
|
||||
&-fields {
|
||||
@include flex(1 0 auto);
|
||||
@include flex(1 0 50%);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&-aside {
|
||||
|
@ -193,6 +195,8 @@
|
|||
min-height: $details-field-line-height;
|
||||
box-sizing: border-box;
|
||||
line-height: $details-field-line-height;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.details__field--editable & {
|
||||
border-radius: $base-border-radius;
|
||||
&:hover {
|
||||
|
@ -220,6 +224,7 @@
|
|||
line-height: $details-field-line-height;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
.details__field--protected & { font-family: $monospace-font-family; }
|
||||
}
|
||||
>textarea {
|
||||
display: block;
|
||||
|
@ -250,7 +255,7 @@
|
|||
&-btn-gen:before { content: $fa-var-bolt; }
|
||||
&-btn-protect {
|
||||
&:before { content: $fa-var-unlock; }
|
||||
&--protected:before { content: $fa-var-lock; }
|
||||
.details__field--protected & { &:before { content: $fa-var-lock; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -480,7 +485,7 @@
|
|||
|
||||
&__tags-autocomplete {
|
||||
position: absolute;
|
||||
@include dropdown;
|
||||
@include common-dropdown;
|
||||
&-tag {
|
||||
padding: $base-padding;
|
||||
display: inline-block;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.gen {
|
||||
position: absolute;
|
||||
@include dropdown;
|
||||
@include common-dropdown;
|
||||
padding: $base-spacing;
|
||||
width: 11em;
|
||||
&__length-range {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
.key-change {
|
||||
@include flex(1);
|
||||
@include display(flex);
|
||||
@include align-items(stretch);
|
||||
@include flex-direction(column);
|
||||
@include justify-content(center);
|
||||
overflow: hidden;
|
||||
padding: $base-spacing;
|
||||
position: relative;
|
||||
@include mobile {
|
||||
padding: $base-padding;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: $modal-icon-size;
|
||||
text-align: center;
|
||||
}
|
||||
&__header {
|
||||
font-size: $small-header-font-size;
|
||||
text-align: center;
|
||||
}
|
||||
&__body {
|
||||
@include flex(0);
|
||||
@include display(flex);
|
||||
@include align-items(flex-start);
|
||||
@include flex-direction(column);
|
||||
margin: $base-spacing 0;
|
||||
}
|
||||
&__input {
|
||||
@include align-self(center);
|
||||
}
|
||||
input[type=password].key-change__pass {
|
||||
font-size: $large-pass-font-size;
|
||||
margin: $small-spacing 0 0;
|
||||
}
|
||||
&__keyfile {
|
||||
@include th { color: muted-color(); }
|
||||
&:hover { @include th { color: medium-color(); } }
|
||||
margin-top: $base-padding-v;
|
||||
cursor: pointer;
|
||||
}
|
||||
&__buttons {
|
||||
text-align: right;
|
||||
button ~ button {
|
||||
margin-left: $small-spacing;
|
||||
}
|
||||
>button {
|
||||
margin-bottom: $small-spacing;
|
||||
}
|
||||
}
|
||||
&__body, &__buttons {
|
||||
@include align-self(center);
|
||||
width: 40%;
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@
|
|||
@include align-items(stretch);
|
||||
@include flex-direction(row);
|
||||
@include justify-content(flex-start);
|
||||
@include flex-wrap(wrap);
|
||||
}
|
||||
&-field-wrap {
|
||||
@include flex(1);
|
||||
|
@ -79,6 +80,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&-adv {
|
||||
@include flex(100%);
|
||||
@include display(flex);
|
||||
@include align-items(stretch);
|
||||
@include flex-direction(row);
|
||||
@include flex-wrap(wrap);
|
||||
&-text {
|
||||
@include flex(100%);
|
||||
}
|
||||
}
|
||||
&-check {
|
||||
@include flex(50%);
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
|
@ -94,9 +108,11 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@include area-selectable(right);
|
||||
&--active, &--active:hover {
|
||||
@include nomobile { @include area-selected(right); }
|
||||
@include nomobile {
|
||||
@include area-selectable(right);
|
||||
&--active, &--active:hover {
|
||||
@include area-selected(right);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.list__item--table) {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
@include flex(1 0 0);
|
||||
@include scrollbar-full-width-hack();
|
||||
@-moz-document url-prefix() { @include scrollbar-padding-hack(); }
|
||||
@at-root { _:-ms-lang(x), .settings>.scroller { @include scrollbar-padding-hack(); } }
|
||||
}
|
||||
|
||||
h2,h3 {
|
||||
|
@ -68,6 +69,10 @@
|
|||
margin-bottom: $base-padding-v;
|
||||
}
|
||||
|
||||
#settings__file-master-pass {
|
||||
font-family: $monospace-font-family;
|
||||
}
|
||||
|
||||
&__file-master-pass-warning {
|
||||
font-weight: normal;
|
||||
float: right;
|
||||
|
|
|
@ -77,7 +77,6 @@ img {
|
|||
|
||||
.thin {
|
||||
font-weight: 200;
|
||||
font-family: $font-family-text-thin;
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Typography
|
||||
$base-font-family: -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
|
||||
$font-family-text-thin: -apple-system, ".SFNSText-Light", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
|
||||
$base-font-family: -apple-system, "BlinkMacSystemFont", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
|
||||
$heading-font-family: $base-font-family;
|
||||
$monospace-font-family: monaco,Consolas,"Lucida Console",monospace;
|
||||
|
||||
|
@ -22,6 +21,7 @@ $base-padding-h: .8em;
|
|||
$base-padding: $base-padding-v $base-padding-h;
|
||||
$medium-padding: .8em 1em;
|
||||
$base-padding-px: 4px 8px;
|
||||
$modal-icon-size: 6em;
|
||||
|
||||
// Borders
|
||||
@function base-border() { @return 1px solid base-border-color(); };
|
||||
|
@ -44,6 +44,8 @@ $base-duration: 150ms;
|
|||
$base-timing: ease;
|
||||
$slow-transition-in: $base-duration*2 ease-in;
|
||||
$slow-transition-out: $base-duration ease-out;
|
||||
$tip-transition-in: 500ms $ease-in-expo;
|
||||
$tip-transition-out: $slow-transition-out;
|
||||
|
||||
// Math
|
||||
$sqrt2: 1.41;
|
||||
|
@ -53,4 +55,4 @@ $z-index-modal: 100000;
|
|||
|
||||
// Screen sizes
|
||||
$tablet-width: 736px;
|
||||
$mobile-width: 414px;
|
||||
$mobile-width: 620px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.pika-single {
|
||||
@include dropdown;
|
||||
@include common-dropdown;
|
||||
}
|
||||
|
||||
.pika-label {
|
||||
|
|
|
@ -10,10 +10,16 @@
|
|||
&__item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
@include area-selectable(right);
|
||||
white-space: nowrap;
|
||||
&--active, &--active:hover {
|
||||
@include area-selected(right);
|
||||
}
|
||||
@include nomobile {
|
||||
@include area-selectable(right);
|
||||
&--active, &--active:hover {
|
||||
@include area-selected(right);
|
||||
}
|
||||
}
|
||||
&-icon {
|
||||
margin-right: $base-padding-h;
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
.help-tip {
|
||||
@include display(flex);
|
||||
@include align-items(stretch);
|
||||
@include flex-direction(row);
|
||||
@include justify-content(flex-start);
|
||||
position: absolute;
|
||||
font-size: 16px;
|
||||
left: 320px;
|
||||
top: 380px;
|
||||
border-radius: $base-border-radius;
|
||||
@include th {
|
||||
color: background-color();
|
||||
background-color: text-color();
|
||||
box-shadow: 0 0 10px rgba(medium-color(), .2);
|
||||
border-bottom: selected-border();
|
||||
}
|
||||
&__side {
|
||||
@include flex(0 0 auto);
|
||||
@include display(flex);
|
||||
@include align-items(stretch);
|
||||
@include flex-direction(column);
|
||||
@include justify-content(center);
|
||||
@include th {
|
||||
background-color: action-color();
|
||||
color: text-color();
|
||||
}
|
||||
cursor: pointer;
|
||||
border-top-left-radius: $base-border-radius;
|
||||
font-size: 16px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
&__text {
|
||||
padding: $base-padding;
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 6em;
|
||||
font-size: $modal-icon-size;
|
||||
text-align: center;
|
||||
}
|
||||
&__header {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
.tip {
|
||||
position: absolute;
|
||||
padding: $base-padding;
|
||||
border-radius: $base-border-radius;
|
||||
white-space: nowrap;
|
||||
z-index: $z-index-no-modal;
|
||||
pointer-events: none;
|
||||
animation: tip $tip-transition-in;
|
||||
@include common-dropdown;
|
||||
&.tip--fast, &.tip--fast:before, &.tip--fast:after {
|
||||
animation-duration: $base-duration;
|
||||
}
|
||||
&--hide.tip, &--hide.tip:before, &--hide.tip:after {
|
||||
transition: all $tip-transition-out;
|
||||
transition-property: color, border-color, background-color, box-shadow;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
&:before, &:after {
|
||||
animation: tip $tip-transition-in;
|
||||
content: " ";
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
$arrow-size-small: 10px 8px;
|
||||
$arrow-size-large: 12px 9px;
|
||||
|
||||
&.tip--bottom:after {
|
||||
@include position(absolute, - nth($arrow-size-small, 2) null null 50%);
|
||||
@include transform(translate(-50%, 0));
|
||||
@include th { @include triangle($arrow-size-small, background-color(), up); }
|
||||
}
|
||||
&.tip--top:after {
|
||||
@include position(absolute, 100% null null 50%);
|
||||
@include transform(translate(-50%, 0));
|
||||
@include th { @include triangle($arrow-size-small, background-color(), down); }
|
||||
}
|
||||
&.tip--left:after {
|
||||
@include position(absolute, 50% null null 100%);
|
||||
@include transform(translate(0, -50%));
|
||||
@include th { @include triangle($arrow-size-small, background-color(), right); }
|
||||
}
|
||||
&.tip--right:after {
|
||||
@include position(absolute, 50% null null (- nth($arrow-size-small, 2)));
|
||||
@include transform(translate(0, -50%));
|
||||
@include th { @include triangle($arrow-size-small, background-color(), left); }
|
||||
}
|
||||
|
||||
&.tip--bottom:before {
|
||||
@include position(absolute, - nth($arrow-size-large, 2) null null 50%);
|
||||
@include transform(translate(-50%, 0));
|
||||
@include th { @include triangle($arrow-size-large, light-border-color(), up); }
|
||||
}
|
||||
&.tip--top:before {
|
||||
@include position(absolute, 100% null null 50%);
|
||||
@include transform(translate(-50%, 0));
|
||||
@include th { @include triangle($arrow-size-large, light-border-color(), down); }
|
||||
}
|
||||
&.tip--left:before {
|
||||
@include position(absolute, 50% null null 100%);
|
||||
@include transform(translate(0, -50%));
|
||||
@include th { @include triangle($arrow-size-large, light-border-color(), right); }
|
||||
}
|
||||
&.tip--right:before {
|
||||
@include position(absolute, 50% null null (- nth($arrow-size-large, 2)));
|
||||
@include transform(translate(0, -50%));
|
||||
@include th { @include triangle($arrow-size-large, light-border-color(), left); }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tip {
|
||||
from { color: transparent; background-color: transparent; border-color: transparent; box-shadow: none; }
|
||||
}
|
|
@ -8,23 +8,24 @@
|
|||
@import "base/base";
|
||||
|
||||
@import "utils/drag";
|
||||
@import "utils/dropdown";
|
||||
@import "utils/common-dropdown";
|
||||
@import "utils/selection";
|
||||
|
||||
@import "common/dates";
|
||||
@import "common/dropdown";
|
||||
@import "common/empty";
|
||||
@import "common/fx";
|
||||
@import "common/help-tip";
|
||||
@import "common/icon-select";
|
||||
@import "common/modal";
|
||||
@import "common/scroll";
|
||||
@import "common/tip";
|
||||
|
||||
@import "areas/app";
|
||||
@import "areas/details";
|
||||
@import "areas/footer";
|
||||
@import "areas/grp";
|
||||
@import "areas/generator";
|
||||
@import "areas/key-change";
|
||||
@import "areas/list";
|
||||
@import "areas/menu";
|
||||
@import "areas/open";
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@mixin dropdown {
|
||||
@mixin common-dropdown {
|
||||
@include th {
|
||||
color: text-color();
|
||||
background: background-color();
|
||||
border: 1px solid light-border-color();
|
||||
box-shadow: dropdown-box-shadow();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<div class="details__attachment-preview">
|
||||
<div class="details__attachment-preview-data"></div>
|
||||
<i class="fa details__attachment-preview-icon"></i>
|
||||
<div class="details__attachment-preview-download-text">{{res 'detAttDownload'}}
|
||||
<span class="details__attachment-preview-download-text-shortcut"></span>{{res 'detAttDelToRemove'}}</div>
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
<div class="details__attachment-preview">
|
||||
<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>
|
|
@ -0,0 +1,3 @@
|
|||
<div class="empty-block muted-color">
|
||||
<h1 class="empty-block__title">{{res 'detEmpty'}}</h1>
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div class="empty-block muted-color">
|
||||
<h1 class="empty-block__title">Your passwords will be displayed here</h1>
|
||||
</div>
|
|
@ -1,5 +1,5 @@
|
|||
<div class="empty-block muted-color">
|
||||
<h1 class="empty-block__title">To restore this group, please drag it to any group outside trash</h1>
|
||||
<h1 class="empty-block__title">{{res 'detGroupRestore'}}</h1>
|
||||
<div class="empty-block__lower-btns">
|
||||
<i class="details__buttons-trash-del fa fa-minus-circle"></i>
|
||||
</div>
|
|
@ -1,22 +1,22 @@
|
|||
<div class="details__history">
|
||||
<div class="details__history-desc muted-color">Click entry history timeline point to view state</div>
|
||||
<div class="details__history-desc muted-color">{{res 'detHistoryClickPoint'}}</div>
|
||||
<div class="details__history-top">
|
||||
<div class="details__history-timeline">
|
||||
<div class="details__history-timeline-axis"></div>
|
||||
<div class="details__history-arrow-prev"><i class="fa fa-long-arrow-left"></i></div>
|
||||
<div class="details__history-arrow-next"><i class="fa fa-long-arrow-right"></i></div>
|
||||
</div>
|
||||
<a class="details__history-close">return to entry <i class="fa fa-external-link-square"></i></a>
|
||||
<a class="details__history-close">{{res 'detHistoryReturn'}} <i class="fa fa-external-link-square"></i></a>
|
||||
</div>
|
||||
<div class="details__history-body">
|
||||
<div class="details__field">
|
||||
<div class="details__field-label">Title</div>
|
||||
<div class="details__field-value"><i class="fa fa-key yellow-color"></i> Agent forum</div>
|
||||
<div class="details__field-label">{{Res 'title'}}</div>
|
||||
<div class="details__field-value"><i class="fa fa-key"></i> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details__history-buttons">
|
||||
<button class="details__history-button details__history-button-revert btn-silent">Revert to state</button>
|
||||
<button class="details__history-button details__history-button-delete btn-error">Delete state</button>
|
||||
<button class="details__history-button details__history-button-discard btn-error">Discard changes</button>
|
||||
<button class="details__history-button details__history-button-revert btn-silent">{{res 'detHistoryRevert'}}</button>
|
||||
<button class="details__history-button details__history-button-delete btn-error">{{res 'detHistoryDel'}}</button>
|
||||
<button class="details__history-button details__history-button-discard btn-error">{{res 'detHistoryDiscard'}}</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,9 +1,9 @@
|
|||
<div class="details">
|
||||
<div class="details__back-button">
|
||||
<i class="fa fa-chevron-left"></i> back to list
|
||||
<i class="fa fa-chevron-left"></i> {{res 'detBackToList'}}
|
||||
</div>
|
||||
<div class="details__header">
|
||||
<i class="details__header-color fa fa-bookmark-o" title="Change icon color">
|
||||
<i class="details__header-color fa fa-bookmark-o" title="{{res 'detSetIconColor'}}" tip-placement="left">
|
||||
<span class="details__colors-popup">
|
||||
<span class="details__colors-popup-item yellow-color fa fa-bookmark-o" data-color="yellow"></span>
|
||||
<span class="details__colors-popup-item green-color fa fa-bookmark-o" data-color="green"></span>
|
||||
|
@ -13,12 +13,12 @@
|
|||
<span class="details__colors-popup-item violet-color fa fa-bookmark-o" data-color="violet"></span>
|
||||
</span>
|
||||
</i>
|
||||
<h1 class="details__header-title"><%- title || '(no title)' %></h1>
|
||||
<% if (customIcon) { %>
|
||||
<div class="details__header-icon details__header-icon--icon" style="background-image: url(<%= customIcon %>)" title="Change icon"></div>
|
||||
<% } else { %>
|
||||
<i class="details__header-icon fa fa-<%= icon %>" title="Change icon"></i>
|
||||
<% } %>
|
||||
<h1 class="details__header-title">{{#if title}}{{title}}{{else}}(no title){{/if}}</h1>
|
||||
{{#if customIcon}}
|
||||
<div class="details__header-icon details__header-icon--icon" style="background-image: url({{{customIcon}}})" title="{{res 'detSetIcon'}}"></div>
|
||||
{{else}}
|
||||
<i class="details__header-icon fa fa-{{icon}}" title="{{res 'detSetIcon'}}"></i>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="details__body">
|
||||
<div class="scroller">
|
||||
|
@ -30,21 +30,23 @@
|
|||
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
|
||||
</div>
|
||||
<div class="details__buttons">
|
||||
<% if (deleted) { %><i class="details__buttons-trash-del fa fa-minus-circle" title="Delete permanently"></i>
|
||||
<% } else { %><i class="details__buttons-trash fa fa-trash-o" title="Delete"></i><% } %>
|
||||
{{#if deleted~}}
|
||||
<i class="details__buttons-trash-del fa fa-minus-circle" title="{{res 'detDelEntryPerm'}}" tip-placement="top"></i>
|
||||
{{~else~}}
|
||||
<i class="details__buttons-trash fa fa-trash-o" title="{{res 'detDelEntry'}}" tip-placement="top"></i>
|
||||
{{~/if~}}
|
||||
<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>
|
||||
<% }); %>
|
||||
<% if (!attachments.length) { %>
|
||||
<div class="details__attachment-add">
|
||||
<span class="details__attachment-add-title">drag attachments here</span> <i class="fa fa-paperclip"></i>
|
||||
</div>
|
||||
<% } %>
|
||||
{{#each attachments as |attachment ix|}}
|
||||
<div class="details__attachment" data-id="{{ix}}"><i class="fa fa-{{attachment.icon}}"></i> {{attachment.title}}</div>
|
||||
{{else}}
|
||||
<div class="details__attachment-add">
|
||||
<span class="details__attachment-add-title">{{res 'detDropAttachments'}}</span> <i class="fa fa-paperclip"></i>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="details__dropzone">
|
||||
<i class="fa fa-paperclip muted-color details__dropzone-icon"></i>
|
||||
<h1 class="muted-color details__dropzone-header">drop attachments here</h1>
|
||||
<h1 class="muted-color details__dropzone-header">{{res 'detDropAttachments'}}</h1>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
<div class="details__field
|
||||
{{~#if editable}} details__field--editable{{/if~}}
|
||||
{{~#if multiline}} details__field--multiline{{/if~}}
|
||||
{{~#if canEditTitle}} details__field--can-edit-title{{/if~}}
|
||||
">
|
||||
<div class="details__field-label">{{title}}</div>
|
||||
<div class="details__field-value">
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
<div class="details__field <%= editable ? 'details__field--editable' : ''
|
||||
%> <%= multiline ? 'details__field--multiline' : ''
|
||||
%> <%= canEditTitle ? 'details__field--can-edit-title' : ''
|
||||
%>">
|
||||
<div class="details__field-label"><%- title %></div>
|
||||
<div class="details__field-value">
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<div class="dropdown">
|
||||
{{#each options as |option|}}
|
||||
<div class="dropdown__item {{#if option.active}}dropdown__item--active{{/if}}" data-value="{{option.value}}">
|
||||
<i class="fa fa-{{option.icon}} dropdown__item-icon"></i>
|
||||
<span class="dropdown__item-text">{{{option.text}}}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
<div class="dropdown">
|
||||
<% options.forEach(function(option) { %>
|
||||
<div class="dropdown__item <%= option.active ? 'dropdown__item--active' : '' %>" data-value="<%- option.value %>">
|
||||
<i class="fa fa-<%= option.icon %> dropdown__item-icon"></i>
|
||||
<span class="dropdown__item-text"><%= option.text %></span>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
<div class="footer">
|
||||
{{#each files.models as |file|}}
|
||||
<div class="footer__db footer__db-item {{#unless file.attributes.open}}footer__db--dimmed{{/unless}}" data-file-id="{{file.cid}}">
|
||||
<i class="fa fa-{{#if file.attributes.open}}unlock{{else}}lock{{/if}}"></i> {{file.attributes.name}}
|
||||
{{#if file.attributes.syncing~}}
|
||||
<i class="fa fa-refresh fa-spin footer__db-sign"></i>
|
||||
{{~else if file.attributes.syncError~}}
|
||||
<i class="fa {{#if file.attributes.modified}}fa-circle{{else}}fa-circle-thin{{/if}} footer__db-sign footer__db-sign--error"
|
||||
title="{{res 'footerSyncError'}}: {{file.attributes.syncError}}"></i>
|
||||
{{~else if file.attributes.modified~}}
|
||||
<i class="fa fa-circle footer__db-sign"></i>
|
||||
{{~/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
<div class="footer__db footer__db--dimmed footer__db--expanded footer__db-open"><i class="fa fa-plus"></i> {{res 'footerOpen'}}</div>
|
||||
<div class="footer__btn footer__btn-help"><i class="fa fa-question"></i></div>
|
||||
<div class="footer__btn footer__btn-settings">
|
||||
{{#if updateAvailable}}
|
||||
<i class="fa fa-bell footer__update-icon"></i>
|
||||
{{else}}
|
||||
<i class="fa fa-cog"></i>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="footer__btn footer__btn-generate"><i class="fa fa-bolt"></i></div>
|
||||
<div class="footer__btn footer__btn-lock"><i class="fa fa-lock"></i></div>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
<div class="footer">
|
||||
<% files.forEach(function(file) { %>
|
||||
<div class="footer__db footer__db-item <%= file.get('open') ? '' : 'footer__db--dimmed' %>" data-file-id="<%= file.cid %>">
|
||||
<i class="fa fa-<%= file.get('open') ? 'unlock' : 'lock' %>"></i> <%- file.get('name') %>
|
||||
<% if (file.get('syncing')) { %><i class="fa fa-refresh fa-spin footer__db-sign"></i><% }
|
||||
else if (file.get('syncError')) { %><i class="fa <%= file.get('modified') ? 'fa-circle' : 'fa-circle-thin' %> footer__db-sign footer__db-sign--error"
|
||||
title="Sync error: <%- file.get('syncError') %>"></i><% }
|
||||
else if (file.get('modified')) { %><i class="fa fa-circle footer__db-sign"></i><% } %>
|
||||
</div>
|
||||
<% }); %>
|
||||
<div class="footer__db footer__db--dimmed footer__db--expanded footer__db-open"><i class="fa fa-plus"></i> Open / New</div>
|
||||
<div class="footer__btn footer__btn-help"><i class="fa fa-question"></i></div>
|
||||
<div class="footer__btn footer__btn-settings">
|
||||
<% if (updateAvailable) { %>
|
||||
<i class="fa fa-bell footer__update-icon"></i>
|
||||
<% } else { %>
|
||||
<i class="fa fa-cog"></i>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="footer__btn footer__btn-generate"><i class="fa fa-bolt"></i></div>
|
||||
<div class="footer__btn footer__btn-lock"><i class="fa fa-lock"></i></div>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
<div class="gen">
|
||||
<div>{{res 'genLen'}}: <span class="gen__length-range-val">{{opt.length}}</span></div>
|
||||
<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" {{#if opt.upper}}checked{{/if}}><label for="gen__check-upper">ABC</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-lower"
|
||||
data-id="lower" {{#if opt.lower}}checked{{/if}}><label for="gen__check-lower">abc</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-digits"
|
||||
data-id="digits" {{#if opt.digits}}checked{{/if}}><label for="gen__check-digits">123</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-special"
|
||||
data-id="special" {{#if opt.special}}checked{{/if}}><label for="gen__check-special">!@#</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-brackets"
|
||||
data-id="brackets" {{#if opt.brackets}}checked{{/if}}><label for="gen__check-brackets">({<</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-high"
|
||||
data-id="high" {{#if opt.high}}checked{{/if}}><label for="gen__check-high">äæ±</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-ambiguous"
|
||||
data-id="ambiguous" {{#if opt.ambiguous}}checked{{/if}}><label for="gen__check-ambiguous">0Oo</label></div>
|
||||
</div>
|
||||
<div class="gen__result"></div>
|
||||
<div class="gen__btn-wrap"><button class="gen__btn-ok">{{btnTitle}}</button></div>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
<div class="gen">
|
||||
<div>Length: <span class="gen__length-range-val"><%= opt.length %></span></div>
|
||||
<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>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-lower"
|
||||
data-id="lower" <%= opt.lower ? 'checked' : '' %>><label for="gen__check-lower">abc</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-digits"
|
||||
data-id="digits" <%= opt.digits ? 'checked' : '' %>><label for="gen__check-digits">123</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-special"
|
||||
data-id="special" <%= opt.special ? 'checked' : '' %>><label for="gen__check-special">!@#</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-brackets"
|
||||
data-id="brackets" <%= opt.brackets ? 'checked' : '' %>><label for="gen__check-brackets">({<</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-high"
|
||||
data-id="high" <%= opt.high ? 'checked' : '' %>><label for="gen__check-high">äæ±</label></div>
|
||||
<div class="gen__check"><input type="checkbox" id="gen__check-ambiguous"
|
||||
data-id="ambiguous" <%= opt.ambiguous ? 'checked' : '' %>><label for="gen__check-ambiguous">0Oo</label></div>
|
||||
</div>
|
||||
<div class="gen__result">password</div>
|
||||
<div class="gen__btn-wrap"><button class="gen__btn-ok"><%= btnTitle %></button></div>
|
||||
</div>
|
|
@ -0,0 +1,32 @@
|
|||
<div class="grp">
|
||||
<div class="grp__back-button">
|
||||
{{res 'retToApp'}} <i class="fa fa-external-link-square"></i>
|
||||
</div>
|
||||
<div class="scroller">
|
||||
<h1>{{res 'grpTitle'}}</h1>
|
||||
<div class="grp__field">
|
||||
<label for="grp__field-title">{{Res 'name'}}:</label>
|
||||
<input type="text" class="input-base" id="grp__field-title" value="{{title}}" size="50" maxlength="1024"
|
||||
required {{#if readonly}}readonly{{/if}} />
|
||||
</div>
|
||||
{{#unless readonly}}
|
||||
<div>
|
||||
<input type="checkbox" class="input-base" id="grp__check-search" {{#if enableSearching}}checked{{/if}} />
|
||||
<label for="grp__check-search">{{res 'grpSearch'}}</label>
|
||||
</div>
|
||||
{{/unless}}
|
||||
<label>{{Res 'icon'}}:</label>
|
||||
{{#if customIcon}}
|
||||
<img src="{{{customIcon}}}" class="grp__icon grp__icon--image" />
|
||||
{{else}}
|
||||
<i class="fa fa-{{icon}} grp__icon"></i>
|
||||
{{/if}}
|
||||
<div class="grp__icons"></div>
|
||||
</div>
|
||||
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
|
||||
{{#unless readonly}}
|
||||
<div class="grp__buttons">
|
||||
<i class="grp__buttons-trash fa fa-trash-o"></i>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
|
@ -1,32 +0,0 @@
|
|||
<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>
|
||||
<% if (!readonly) { %>
|
||||
<div>
|
||||
<input type="checkbox" class="input-base" id="grp__check-search" <%= enableSearching ? 'checked' : '' %> />
|
||||
<label for="grp__check-search">Enable searching entries in this group</label>
|
||||
</div>
|
||||
<% } %>
|
||||
<label>Icon:</label>
|
||||
<% if (customIcon) { %>
|
||||
<img src="<%= customIcon %>" class="grp__icon grp__icon--image" />
|
||||
<% } else { %>
|
||||
<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>
|
|
@ -1,4 +0,0 @@
|
|||
<div class="help-tip">
|
||||
<div class="help-tip__side"><i class="fa fa-lightbulb-o"></i></div>
|
||||
<div class="help-tip__text"><%- text %></div>
|
||||
</div>
|
|
@ -1,26 +1,26 @@
|
|||
<div class="icon-select">
|
||||
<div class="icon-select__items">
|
||||
<% icons.forEach(function(icon, ix) { %>
|
||||
<i class="fa fa-<%= icon %> icon-select__icon <%= ix === sel ? 'icon-select__icon--active' : '' %>" data-val="<%= ix %>"></i>
|
||||
<% }); %>
|
||||
{{#each icons as |icon ix|}}
|
||||
<i class="fa fa-{{icon}} icon-select__icon {{#ifeq ix sel}}icon-select__icon--active{{/ifeq}}" data-val="{{ix}}"></i>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="icon-select__items icon-select__items--custom">
|
||||
<input type="file" class="icon-select__file-input hide-by-pos" accept="image/*" />
|
||||
<% if (canDownloadFavicon) { %>
|
||||
{{#if canDownloadFavicon}}
|
||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-download"
|
||||
data-val="special" data-special="download" title="Download and use website favicon">
|
||||
data-val="special" data-special="download" title="{{res 'iconFavTitle'}}">
|
||||
<i class="fa fa-cloud-download"></i>
|
||||
</span>
|
||||
<% } %>
|
||||
{{/if}}
|
||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-select"
|
||||
data-val="special" data-special="select" title="Select custom icon">
|
||||
data-val="special" data-special="select" title="{{res 'iconSelCustom'}}">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</span>
|
||||
<% Object.keys(customIcons).forEach(function(ci) { %>
|
||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom <%= ci === sel ? 'icon-select__icon--active' : '' %>"
|
||||
data-val="<%- ci %>">
|
||||
<img src="<%= customIcons[ci] %>" />
|
||||
{{#each customIcons as |icon ci|}}
|
||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci sel}}icon-select__icon--active{{/ifeq}}"
|
||||
data-val="{{ci}}">
|
||||
<img src="{{{icon}}}" />
|
||||
</span>
|
||||
<% }); %>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
<div class="key-change">
|
||||
<i class="key-change__icon fa fa-lock"></i>
|
||||
<div class="key-change__header">{{fileName}}: {{res 'keyChangeTitle'}}</div>
|
||||
<div class="key-change__body">
|
||||
<div class="key-change__message">{{res 'keyChangeMessage'}}:</div>
|
||||
<div class="key-change__input">
|
||||
<input class="key-change__file hide-by-pos" type="file" />
|
||||
<input class="key-change__pass" type="password" size="30" autocomplete="off" maxlength="128" autofocus />
|
||||
<div class="key-change__keyfile">
|
||||
<i class="fa fa-key"></i> {{res 'openKeyFile'}}<span class="key-change__keyfile-name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="key-change__buttons">
|
||||
<button class="key-change__btn-ok" data-result="ok">{{res 'alertOk'}}</button>
|
||||
<button class="btn-error key-change__btn-cancel" data-result="">{{res 'alertCancel'}}</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
<div class="empty-block muted-color">
|
||||
<div class="empty-block__icon"><i class="fa fa-key"></i></div>
|
||||
<h1 class="empty-block__title">Empty</h1>
|
||||
<h1 class="empty-block__title">{{res 'listEmptyTitle'}}</h1>
|
||||
<p class="empty-block__text">
|
||||
add with <i class="fa fa-plus"></i> button above
|
||||
{{#res 'listEmptyAdd'}} <i class="fa fa-plus"></i>{{/res}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<div class="list__item {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
|
||||
{{#if customIcon~}}
|
||||
<img src="{{{customIcon}}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
{{~else~}}
|
||||
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
|
||||
{{~/if}}
|
||||
<span class="list__item-title">{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</span><span class="list__item-descr thin">{{description}}</span>
|
||||
</div>
|
|
@ -1,5 +0,0 @@
|
|||
<div class="list__item <%= active ? 'list__item--active' : '' %> <%= expired ? 'list__item--expired' : '' %>" id="<%= id %>" draggable="true">
|
||||
<% if (customIcon) { %><img src="<%= customIcon %>" class="list__item-icon list__item-icon--custom <%= color || '' %>" /><% }
|
||||
else { %><i class="fa fa-<%= icon %> <%= color ? color+'-color' : '' %> list__item-icon"></i><% } %>
|
||||
<span class="list__item-title"><%- title || '(no title)' %></span><span class="list__item-descr thin"><%- description %></span>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<tr class="list__item list__item--table {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
|
||||
<td>
|
||||
{{~#if customIcon~}}
|
||||
<img src="{{{customIcon}}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
{{~else~}}
|
||||
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
|
||||
{{~/if~}}
|
||||
</td>
|
||||
<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>
|
||||
<td>{{user}}</td>
|
||||
<td>{{url}}</td>
|
||||
<td>{{tags}}</td>
|
||||
<td>{{notes}}</td>
|
||||
</tr>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue