Merge branch 'develop'

This commit is contained in:
Antelle 2016-01-19 22:27:44 +03:00
commit f315792efc
130 changed files with 2196 additions and 933 deletions

View File

@ -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: {

View File

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

View File

@ -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
});

View File

@ -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],

View File

@ -51,6 +51,7 @@ var CopyPaste = {
}).bind(null, Launcher.getClipboardText()), clipboardSeconds * 1000);
}, 0);
}
return clipboardSeconds;
}
}
};

View File

@ -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;
}

View File

@ -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) {

View File

@ -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));
}
});

View File

@ -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();

View File

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

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -0,0 +1,7 @@
'use strict';
require('./cmp');
require('./ifeq');
require('./ifneq');
require('./ifemptyoreq');
require('./res');

View File

@ -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;
});

View File

@ -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;

View File

@ -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);
}
}
};

View File

@ -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,

View File

@ -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);

View File

@ -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});
}
},

View File

@ -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();
},

View File

@ -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();
},

View File

@ -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([

View File

@ -10,6 +10,7 @@ var UpdateModel = Backbone.Model.extend({
lastVersion: null,
lastVersionReleaseDate: null,
lastCheckError: null,
lastCheckUpdMin: null,
status: null,
updateStatus: null,
updateError: null,

View File

@ -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;
}

View File

@ -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() : '';
}
};

View File

@ -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 &rarr; Z',
searchZA: 'Z &rarr; A',
searchON: 'Old &rarr; New',
searchNO: 'New &rarr; 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;

140
app/scripts/util/tip.js Normal file
View File

@ -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;

View File

@ -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);
}
});

View File

@ -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: {
},

View File

@ -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) {

View File

@ -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();

View File

@ -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'

View File

@ -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(); });
}
});

View File

@ -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));

View File

@ -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>';
},

View File

@ -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

View File

@ -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(); });
}
});

View File

@ -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);
}

View File

@ -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, '');
}
});

View File

@ -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);

View File

@ -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;
},

View File

@ -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);

View File

@ -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;
},

View File

@ -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;
},

View File

@ -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;

View File

@ -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 &rarr; Z' },
{ value: '-title', icon: 'sort-alpha-desc', text: 'Title Z &rarr; A' },
{ value: 'website', icon: 'sort-alpha-asc', text: 'Website A &rarr; Z' },
{ value: '-website', icon: 'sort-alpha-desc', text: 'Website Z &rarr; A' },
{ value: 'user', icon: 'sort-alpha-asc', text: 'User A &rarr; Z' },
{ value: '-user', icon: 'sort-alpha-desc', text: 'User Z &rarr; A' },
{ value: 'created', icon: 'sort-numeric-asc', text: 'Created Old &rarr; New' },
{ value: '-created', icon: 'sort-numeric-desc', text: 'Created New &rarr; Old' },
{ value: 'updated', icon: 'sort-numeric-asc', text: 'Updated Old &rarr; New' },
{ value: '-updated', icon: 'sort-numeric-desc', text: 'Updated New &rarr; 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();

View File

@ -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');
}
},

View File

@ -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');

View File

@ -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();

View File

@ -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: {},

View File

@ -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',

View File

@ -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: '',

View File

@ -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({

View File

@ -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') {

View File

@ -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;
}
},

View File

@ -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' +

View File

@ -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({

View File

@ -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;
},

View File

@ -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);
}

View File

@ -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;

View File

@ -1,6 +1,6 @@
.gen {
position: absolute;
@include dropdown;
@include common-dropdown;
padding: $base-spacing;
width: 11em;
&__length-range {

View File

@ -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%;
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -77,7 +77,6 @@ img {
.thin {
font-weight: 200;
font-family: $font-family-text-thin;
}
* {

View File

@ -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;

View File

@ -1,5 +1,5 @@
.pika-single {
@include dropdown;
@include common-dropdown;
}
.pika-label {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -24,7 +24,7 @@
}
&__icon {
font-size: 6em;
font-size: $modal-icon-size;
text-align: center;
}
&__header {

View File

@ -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; }
}

View File

@ -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";

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,3 @@
<div class="empty-block muted-color">
<h1 class="empty-block__title">{{res 'detEmpty'}}</h1>
</div>

View File

@ -1,3 +0,0 @@
<div class="empty-block muted-color">
<h1 class="empty-block__title">Your passwords will be displayed here</h1>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

26
app/templates/footer.hbs Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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">({&lt;</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>

View File

@ -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">({&lt;</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>

32
app/templates/grp.hbs Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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