mirror of https://github.com/keeweb/keeweb.git
Merge branch 'develop'
This commit is contained in:
commit
2338ca589c
|
@ -9,3 +9,6 @@ tmp/
|
|||
build/
|
||||
coverage/
|
||||
keys/
|
||||
*.user
|
||||
bin/
|
||||
obj/
|
||||
|
|
81
Gruntfile.js
81
Gruntfile.js
|
@ -16,9 +16,9 @@ module.exports = function(grunt) {
|
|||
var webpack = require('webpack');
|
||||
var pkg = require('./package.json');
|
||||
var dt = new Date().toISOString().replace(/T.*/, '');
|
||||
var electronVersion = '0.37.4';
|
||||
var minElectronVersionForUpdate = '0.32.0';
|
||||
var minElectronVersionForUpdate = '1.0.1';
|
||||
var zipCommentPlaceholder = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
|
||||
var electronVersion = pkg.devDependencies['electron-prebuilt'].replace(/^\D/, '');
|
||||
|
||||
while (zipCommentPlaceholder.length < 512) {
|
||||
zipCommentPlaceholder += '.';
|
||||
|
@ -98,12 +98,12 @@ module.exports = function(grunt) {
|
|||
nonull: true
|
||||
},
|
||||
'desktop_osx': {
|
||||
src: 'tmp/desktop/KeeWeb.dmg',
|
||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb-' + pkg.version + '.dmg',
|
||||
dest: 'dist/desktop/KeeWeb.mac.dmg',
|
||||
nonull: true
|
||||
},
|
||||
'desktop_win': {
|
||||
src: 'tmp/desktop/KeeWeb Setup.exe',
|
||||
src: 'tmp/desktop/win/KeeWebSetup-' + pkg.version + '-ia32.exe',
|
||||
dest: 'dist/desktop/KeeWeb.win32.exe',
|
||||
nonull: true
|
||||
},
|
||||
|
@ -253,6 +253,9 @@ module.exports = function(grunt) {
|
|||
Buffer: false,
|
||||
__filename: false,
|
||||
__dirname: false
|
||||
},
|
||||
externals: {
|
||||
xmldom: 'null'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -299,13 +302,6 @@ module.exports = function(grunt) {
|
|||
'app-version': pkg.version,
|
||||
'build-version': '<%= gitinfo.local.branch.current.shortSHA %>'
|
||||
},
|
||||
osx: {
|
||||
options: {
|
||||
platform: 'darwin',
|
||||
arch: 'x64',
|
||||
icon: 'graphics/app.icns'
|
||||
}
|
||||
},
|
||||
linux64: {
|
||||
options: {
|
||||
platform: 'linux',
|
||||
|
@ -319,58 +315,40 @@ module.exports = function(grunt) {
|
|||
arch: 'ia32',
|
||||
icon: 'graphics/app.ico'
|
||||
}
|
||||
},
|
||||
win32: {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
arch: 'ia32',
|
||||
icon: 'graphics/app.ico',
|
||||
'version-string': {
|
||||
CompanyName: 'antelle.github.io',
|
||||
LegalCopyright: 'Antelle, MIT license',
|
||||
FileDescription: 'KeeWeb Desktop',
|
||||
OriginalFilename: 'KeeWeb.exe',
|
||||
FileVersion: pkg.version,
|
||||
ProductVersion: pkg.version,
|
||||
ProductName: 'KeeWeb',
|
||||
InternalName: 'KeeWeb'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'electron-builder': {
|
||||
options: {
|
||||
out: path.join(__dirname, 'tmp/desktop'),
|
||||
basePath: __dirname,
|
||||
config: {
|
||||
osx: {
|
||||
title: 'KeeWeb',
|
||||
background: path.join(__dirname, 'graphics/dmg-bg.png'),
|
||||
icon: path.join(__dirname, 'graphics/app.icns'),
|
||||
'icon-size': 80,
|
||||
contents: [
|
||||
{'x': 438, 'y': 344, 'type': 'link', 'path': '/Applications'},
|
||||
{'x': 192, 'y': 344, 'type': 'file'}
|
||||
]
|
||||
},
|
||||
win: {
|
||||
title: 'KeeWeb',
|
||||
icon: path.join(__dirname, 'graphics/app.ico')
|
||||
}
|
||||
}
|
||||
publish: 'never',
|
||||
dist: false,
|
||||
projectDir: __dirname,
|
||||
appDir: 'tmp/desktop/app',
|
||||
sign: false
|
||||
},
|
||||
osx: {
|
||||
options: {
|
||||
platform: 'osx',
|
||||
appPath: path.join(__dirname, 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app')
|
||||
platforms: ['osx'],
|
||||
arch: 'x64'
|
||||
}
|
||||
},
|
||||
win: {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
appPath: path.join(__dirname, 'tmp/desktop/KeeWeb-win32-ia32')
|
||||
platform: ['win32'],
|
||||
arch: 'ia32'
|
||||
}
|
||||
}
|
||||
// linux64: {
|
||||
// options: {
|
||||
// platform: ['linux'],
|
||||
// arch: 'x64'
|
||||
// }
|
||||
// },
|
||||
// linux32: {
|
||||
// options: {
|
||||
// platform: ['linux'],
|
||||
// arch: 'ia32'
|
||||
// }
|
||||
// }
|
||||
},
|
||||
compress: {
|
||||
linux64: {
|
||||
|
@ -488,7 +466,8 @@ module.exports = function(grunt) {
|
|||
'sign-archive:desktop_update',
|
||||
'validate-desktop-update',
|
||||
'electron',
|
||||
'electron-builder',
|
||||
'electron-builder:osx',
|
||||
'electron-builder:win',
|
||||
'compress:linux64',
|
||||
'compress:linux32',
|
||||
'deb:linux64',
|
||||
|
|
|
@ -8,38 +8,83 @@ var AppModel = require('./models/app-model'),
|
|||
Alerts = require('./comp/alerts'),
|
||||
Updater = require('./comp/updater'),
|
||||
AuthReceiver = require('./comp/auth-receiver'),
|
||||
ExportApi = require('./comp/export-api'),
|
||||
ThemeChanger = require('./util/theme-changer'),
|
||||
Locale = require('./util/locale');
|
||||
|
||||
$(function() {
|
||||
if ((window.parent !== window.top) || window.opener) {
|
||||
if (isPopup()) {
|
||||
return AuthReceiver.receive();
|
||||
}
|
||||
require('./mixins/view');
|
||||
require('./helpers');
|
||||
KeyHandler.init();
|
||||
IdleTracker.init();
|
||||
PopupNotifier.init();
|
||||
loadMixins();
|
||||
initModules();
|
||||
|
||||
var appModel = new AppModel();
|
||||
if (appModel.settings.get('theme')) {
|
||||
ThemeChanger.setTheme(appModel.settings.get('theme'));
|
||||
}
|
||||
var skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
|
||||
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0 && !skipHttpsWarning) {
|
||||
Alerts.error({ header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
buttons: [
|
||||
{ result: '', title: Locale.appSecWarnBtn, error: true }
|
||||
],
|
||||
complete: showApp
|
||||
ThemeChanger.setBySettings(appModel.settings);
|
||||
var configParam = getConfigParam();
|
||||
if (configParam) {
|
||||
appModel.loadConfig(configParam, function(err) {
|
||||
if (err) {
|
||||
showSettingsLoadError();
|
||||
} else {
|
||||
ThemeChanger.setBySettings(appModel.settings);
|
||||
showApp();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showApp();
|
||||
}
|
||||
|
||||
function isPopup() {
|
||||
return (window.parent !== window.top) || window.opener;
|
||||
}
|
||||
|
||||
function loadMixins() {
|
||||
require('./mixins/view');
|
||||
require('./helpers');
|
||||
}
|
||||
|
||||
function initModules() {
|
||||
KeyHandler.init();
|
||||
IdleTracker.init();
|
||||
PopupNotifier.init();
|
||||
window.kw = ExportApi;
|
||||
}
|
||||
|
||||
function showSettingsLoadError() {
|
||||
ThemeChanger.setBySettings(appModel.settings);
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appSettingsErrorBody,
|
||||
buttons: [],
|
||||
esc: false, enter: false, click: false
|
||||
});
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
var skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
|
||||
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0 && !skipHttpsWarning) {
|
||||
Alerts.error({ header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
buttons: [
|
||||
{ result: '', title: Locale.appSecWarnBtn, error: true }
|
||||
],
|
||||
complete: showView
|
||||
});
|
||||
} else {
|
||||
showView();
|
||||
}
|
||||
}
|
||||
|
||||
function showView() {
|
||||
new AppView({ model: appModel }).render();
|
||||
Updater.init();
|
||||
}
|
||||
|
||||
function getConfigParam() {
|
||||
var match = location.search.match(/[\?&]config=([^&]+)/i);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../comp/launcher');
|
||||
|
||||
var AutoTypeEmitterFactory = {
|
||||
create: function(callback) {
|
||||
if (!Launcher) {
|
||||
return null;
|
||||
}
|
||||
var AutoTypeEmitter = require('./emitter/auto-type-emitter-' + Launcher.platform());
|
||||
return new AutoTypeEmitter(callback);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitterFactory;
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../comp/launcher');
|
||||
|
||||
var AutoTypeHelperFactory = {
|
||||
create: function() {
|
||||
if (!Launcher) {
|
||||
return null;
|
||||
}
|
||||
var AutoTypeHelper = require('./helper/auto-type-helper-' + Launcher.platform());
|
||||
return new AutoTypeHelper();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelperFactory;
|
|
@ -0,0 +1,203 @@
|
|||
'use strict';
|
||||
|
||||
var Logger = require('../util/logger');
|
||||
|
||||
var logger = new Logger('auto-type-obfuscator');
|
||||
logger.setLevel(localStorage.autoTypeDebug ? Logger.Level.All : Logger.Level.Warn);
|
||||
|
||||
var MaxFakeOps = 30;
|
||||
var MaxSteps = 1000;
|
||||
var MaxCopy = 2;
|
||||
var FakeCharAlphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz123456789O0oIl';
|
||||
|
||||
var AutoTypeObfuscator = function(chars) {
|
||||
this.chars = chars;
|
||||
this.inputChars = [];
|
||||
this.inputCursor = 0;
|
||||
this.inputSel = 0;
|
||||
this.ops = [];
|
||||
this.stepCount = 0;
|
||||
this.copyCount = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.obfuscate = function() {
|
||||
while (!this.finished()) {
|
||||
this.step();
|
||||
if (this.stepCount++ > MaxSteps) {
|
||||
throw 'Obfuscate failed';
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < this.chars.length; i++) {
|
||||
this.chars[i] = null;
|
||||
this.inputChars[i] = null;
|
||||
}
|
||||
return this.ops;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.finished = function() {
|
||||
return this.chars.length === this.inputChars.length &&
|
||||
this.chars.every(function(ch, ix) { return this.inputChars[ix].ch === ch; }, this);
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.step = function() {
|
||||
var isFake = this.stepCount < MaxFakeOps && Math.random() > this.stepCount / MaxFakeOps;
|
||||
if (isFake) {
|
||||
this.stepFake();
|
||||
} else {
|
||||
this.stepReal();
|
||||
}
|
||||
if (logger.getLevel() >= Logger.Level.Debug) {
|
||||
logger.debug('value', this.inputChars.map(function(ic) { return ic.ch; }).join(''));
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.stepFake = function() {
|
||||
var pos = Math.floor(Math.random() * (this.inputChars.length + 1));
|
||||
var ch = FakeCharAlphabet[Math.floor(Math.random() * FakeCharAlphabet.length)];
|
||||
logger.info('step.fake', pos, ch);
|
||||
this.moveToPos(pos);
|
||||
var insert = this.inputChars.length === 0 || Math.random() > 0.3;
|
||||
if (insert) {
|
||||
this.inputChar(ch);
|
||||
} else {
|
||||
var moveLeft = Math.random() > 0.5;
|
||||
var maxMove = moveLeft ? pos : this.inputChars.length - pos;
|
||||
if (maxMove === 0) {
|
||||
moveLeft = !moveLeft;
|
||||
maxMove = moveLeft ? pos : this.inputChars.length - pos;
|
||||
}
|
||||
var moveCount = Math.max(Math.floor(Math.pow(Math.random(), 3) * maxMove), 1);
|
||||
if (moveCount <= 1 && Math.random() > 0.5) {
|
||||
this.deleteText(moveLeft);
|
||||
} else {
|
||||
this.selectText(moveLeft, moveCount);
|
||||
if (Math.random() > 0.3) {
|
||||
this.deleteText(Math.random() > 0.5);
|
||||
} else {
|
||||
this.inputChar(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.stepReal = function() {
|
||||
var possibleActions = [];
|
||||
var inputRealPositions = [];
|
||||
var i;
|
||||
for (i = 0; i < this.chars.length; i++) {
|
||||
inputRealPositions.push(-1);
|
||||
}
|
||||
for (i = 0; i < this.inputChars.length; i++) {
|
||||
var ix = this.inputChars[i].ix;
|
||||
if (ix === undefined) {
|
||||
possibleActions.push({ del: true, pos: i });
|
||||
} else {
|
||||
inputRealPositions[ix] = i;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < this.chars.length; i++) {
|
||||
if (inputRealPositions[i] < 0) {
|
||||
var from = 0, to = this.inputChars.length;
|
||||
for (var j = 0; j < this.chars.length; j++) {
|
||||
if (j < i && inputRealPositions[j] >= 0) {
|
||||
from = inputRealPositions[j] + 1;
|
||||
}
|
||||
if (j > i && inputRealPositions[j] >= 0) {
|
||||
to = inputRealPositions[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
possibleActions.push({ ins: true, ch: this.chars[i], ix: i, from: from, to: to });
|
||||
}
|
||||
}
|
||||
var action = possibleActions[Math.floor(Math.random() * possibleActions.length)];
|
||||
logger.info('step.real', inputRealPositions, action);
|
||||
if (action.del) {
|
||||
this.moveToPos(action.pos + 1);
|
||||
this.deleteText(true);
|
||||
} else {
|
||||
var insPos = action.from + Math.floor(Math.random() * (action.to - action.from));
|
||||
this.moveToPos(insPos);
|
||||
if (this.copyCount < MaxCopy && action.ch !== '\n' && Math.random() > 0.5) {
|
||||
this.copyCount++;
|
||||
this.copyPaste(action.ch);
|
||||
} else {
|
||||
this.inputChar(action.ch);
|
||||
}
|
||||
this.inputChars[insPos].ix = action.ix;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveToPos = function(pos) {
|
||||
logger.debug('moveToPos', pos);
|
||||
while (this.inputCursor > pos) {
|
||||
this.moveLeft();
|
||||
}
|
||||
while (this.inputCursor < pos) {
|
||||
this.moveRight();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveLeft = function() {
|
||||
logger.debug('moveLeft');
|
||||
this.ops.push({ type: 'key', value: 'left' });
|
||||
this.inputCursor--;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveRight = function() {
|
||||
logger.debug('moveRight');
|
||||
this.ops.push({ type: 'key', value: 'right' });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.inputChar = function(ch) {
|
||||
logger.debug('inputChar', ch);
|
||||
this.ops.push({ type: 'text', value: ch });
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.copyPaste = function(ch) {
|
||||
logger.debug('copyPaste', ch);
|
||||
this.ops.push({type: 'cmd', value: 'copyPaste', arg: ch});
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.selectText = function(backward, count) {
|
||||
logger.debug('selectText', backward ? 'left' : 'right', count);
|
||||
var ops = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
ops.push({ type: 'key', value: backward ? 'left' : 'right' });
|
||||
}
|
||||
if (ops.length === 1) {
|
||||
ops[0].mod = {'+': true};
|
||||
this.ops.push(ops[0]);
|
||||
} else {
|
||||
this.ops.push({type: 'group', value: ops, mod: {'+': true}});
|
||||
}
|
||||
if (backward) {
|
||||
this.inputCursor -= count;
|
||||
}
|
||||
this.inputSel = count;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.deleteText = function(backward) {
|
||||
logger.debug('deleteText', backward ? 'left' : 'right');
|
||||
this.ops.push({ type: 'key', value: backward ? 'bs' : 'del' });
|
||||
if (this.inputSel) {
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel);
|
||||
this.inputSel = 0;
|
||||
} else {
|
||||
this.inputChars.splice(backward ? this.inputCursor - 1 : this.inputCursor, 1);
|
||||
if (backward) {
|
||||
this.inputCursor--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeObfuscator;
|
|
@ -0,0 +1,136 @@
|
|||
'use strict';
|
||||
|
||||
var AutoTypeRunner = require('./auto-type-runner');
|
||||
|
||||
var AutoTypeParser = function(sequence) {
|
||||
this.sequence = sequence;
|
||||
this.ix = 0;
|
||||
this.states = [];
|
||||
};
|
||||
|
||||
AutoTypeParser.opSepRegex = /[\s:=]+/;
|
||||
|
||||
AutoTypeParser.prototype.parse = function() {
|
||||
var len = this.sequence.length;
|
||||
this.pushState();
|
||||
while (this.ix < len) {
|
||||
var ch = this.sequence[this.ix];
|
||||
switch (ch) {
|
||||
case '{':
|
||||
this.readOp();
|
||||
continue;
|
||||
case '+':
|
||||
case '%':
|
||||
case '^':
|
||||
this.readModifier(ch);
|
||||
break;
|
||||
case '(':
|
||||
this.pushState();
|
||||
break;
|
||||
case ')':
|
||||
this.popState();
|
||||
break;
|
||||
case ' ':
|
||||
break;
|
||||
case '~':
|
||||
this.addOp('enter');
|
||||
break;
|
||||
default:
|
||||
this.addChar(ch);
|
||||
break;
|
||||
}
|
||||
this.ix++;
|
||||
}
|
||||
if (this.states.length !== 1) {
|
||||
throw 'Groups count mismatch';
|
||||
}
|
||||
return new AutoTypeRunner(this.state().ops);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.pushState = function() {
|
||||
this.states.unshift({
|
||||
modifiers: null,
|
||||
ops: []
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.popState = function() {
|
||||
if (this.states.length <= 1) {
|
||||
throw 'Unexpected ")" at index ' + this.ix;
|
||||
}
|
||||
var state = this.states.shift();
|
||||
this.addState(state);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.state = function() {
|
||||
return this.states[0];
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.readOp = function() {
|
||||
var toIx = this.sequence.indexOf('}', this.ix + 2);
|
||||
if (toIx < 0) {
|
||||
throw 'Mismatched "{" at index ' + this.ix;
|
||||
}
|
||||
var contents = this.sequence.substring(this.ix + 1, toIx);
|
||||
this.ix = toIx + 1;
|
||||
if (contents.length === 1) {
|
||||
this.addChar(contents);
|
||||
return;
|
||||
}
|
||||
var parts = contents.split(AutoTypeParser.opSepRegex, 2);
|
||||
if (parts.length > 1 && parts[0].length && parts[1].length) {
|
||||
var op = parts[0];
|
||||
var sep = contents.substr(op.length, 1);
|
||||
var arg = parts[1];
|
||||
this.addOp(op, sep, arg);
|
||||
} else {
|
||||
this.addOp(contents);
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.readModifier = function(modifier) {
|
||||
var state = this.state();
|
||||
if (!state.modifiers) {
|
||||
state.modifiers = {};
|
||||
}
|
||||
if (modifier === '^' && state.modifiers['^']) {
|
||||
delete state.modifiers['^'];
|
||||
modifier = '^^';
|
||||
}
|
||||
state.modifiers[modifier] = true;
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.resetModifiers = function() {
|
||||
var state = this.state();
|
||||
var modifiers = state.modifiers;
|
||||
state.modifiers = null;
|
||||
return modifiers;
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addState = function(state) {
|
||||
this.state().ops.push({
|
||||
type: 'group',
|
||||
value: state.ops,
|
||||
mod: this.resetModifiers()
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addChar = function(ch) {
|
||||
this.state().ops.push({
|
||||
type: 'text',
|
||||
value: ch,
|
||||
mod: this.resetModifiers()
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addOp = function(op, sep, arg) {
|
||||
this.state().ops.push({
|
||||
type: 'op',
|
||||
value: op,
|
||||
mod: this.resetModifiers(),
|
||||
sep: sep,
|
||||
arg: arg
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeParser;
|
|
@ -0,0 +1,437 @@
|
|||
'use strict';
|
||||
|
||||
var AutoTypeObfuscator = require('./auto-type-obfuscator'),
|
||||
AutoTypeEmitterFactory = require('./auto-type-emitter-factory'),
|
||||
Format = require('../util/format'),
|
||||
Logger = require('../util/logger');
|
||||
|
||||
var emitterLogger = new Logger('auto-type-emitter');
|
||||
emitterLogger.setLevel(localStorage.autoTypeDebug ? Logger.Level.All : Logger.Level.Warn);
|
||||
|
||||
var AutoTypeRunner = function(ops) {
|
||||
this.ops = ops;
|
||||
this.pendingResolvesCount = 0;
|
||||
this.entry = null;
|
||||
this.now = new Date();
|
||||
};
|
||||
|
||||
AutoTypeRunner.PendingResolve = { pending: true };
|
||||
|
||||
AutoTypeRunner.Keys = {
|
||||
tab: 'tab', enter: 'enter', space: 'space',
|
||||
up: 'up', down: 'down', left: 'left', right: 'right', home: 'home', end: 'end', pgup: 'pgup', pgdn: 'pgdn',
|
||||
insert: 'ins', ins: 'ins', delete: 'del', del: 'del', backspace: 'bs', bs: 'bs', bksp: 'bs', esc: 'esc',
|
||||
win: 'win', lwin: 'win', rwin: 'rwin', f1: 'f1', f2: 'f2', f3: 'f3', f4: 'f4', f5: 'f5', f6: 'f6',
|
||||
f7: 'f7', f8: 'f8', f9: 'f9', f10: 'f10', f11: 'f11', f12: 'f12', f13: 'f13', f14: 'f14', f15: 'f15', f16: 'f16',
|
||||
add: 'add', subtract: 'subtract', multiply: 'multiply', divide: 'divide',
|
||||
numpad0: 'n0', numpad1: 'n1', numpad2: 'n2', numpad3: 'n3', numpad4: 'n4',
|
||||
numpad5: 'n5', numpad6: 'n6', numpad7: 'n7', numpad8: 'n8', numpad9: 'n9'
|
||||
};
|
||||
|
||||
AutoTypeRunner.Substitutions = {
|
||||
title: function(runner, op) { return runner.getEntryFieldKeys('Title', op); },
|
||||
username: function(runner, op) { return runner.getEntryFieldKeys('UserName', op); },
|
||||
url: function(runner, op) { return runner.getEntryFieldKeys('URL', op); },
|
||||
password: function(runner, op) { return runner.getEntryFieldKeys('Password', op); },
|
||||
notes: function(runner, op) { return runner.getEntryFieldKeys('Notes', op); },
|
||||
group: function(runner) { return runner.getEntryGroupName(); },
|
||||
totp: function(runner, op) { return runner.getOtp(op); },
|
||||
s: function(runner, op) { return runner.getEntryFieldKeys(op.arg, op); },
|
||||
'dt_simple': function(runner) { return runner.dt('simple'); },
|
||||
'dt_year': function(runner) { return runner.dt('Y'); },
|
||||
'dt_month': function(runner) { return runner.dt('M'); },
|
||||
'dt_day': function(runner) { return runner.dt('D'); },
|
||||
'dt_hour': function(runner) { return runner.dt('h'); },
|
||||
'dt_minute': function(runner) { return runner.dt('m'); },
|
||||
'dt_second': function(runner) { return runner.dt('s'); },
|
||||
'dt_utc_simple': function(runner) { return runner.udt('simple'); },
|
||||
'dt_utc_year': function(runner) { return runner.udt('Y'); },
|
||||
'dt_utc_month': function(runner) { return runner.udt('M'); },
|
||||
'dt_utc_day': function(runner) { return runner.udt('D'); },
|
||||
'dt_utc_hour': function(runner) { return runner.udt('h'); },
|
||||
'dt_utc_minute': function(runner) { return runner.udt('m'); },
|
||||
'dt_utc_second': function(runner) { return runner.udt('s'); }
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolve = function(entry, callback) {
|
||||
this.entry = entry;
|
||||
try {
|
||||
this.resolveOps(this.ops);
|
||||
if (!this.pendingResolvesCount) {
|
||||
callback();
|
||||
} else {
|
||||
this.resolveCallback = callback;
|
||||
}
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolveOps = function(ops) {
|
||||
for (var i = 0, len = ops.length; i < len; i++) {
|
||||
var op = ops[i];
|
||||
if (op.type === 'group') {
|
||||
this.resolveOps(op.value);
|
||||
} else {
|
||||
this.resolveOp(op);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
if (op.value.length === 1 && !op.sep) {
|
||||
// {x}
|
||||
op.type = 'text';
|
||||
return;
|
||||
}
|
||||
if (op.value.length === 1 && op.sep === ' ') {
|
||||
// {x 3}
|
||||
op.type = 'text';
|
||||
var ch = op.value, text = ch, len = +op.arg;
|
||||
while (text.length < len) {
|
||||
text += ch;
|
||||
}
|
||||
op.value = text;
|
||||
return;
|
||||
}
|
||||
var lowerValue = op.value.toLowerCase();
|
||||
var key = AutoTypeRunner.Keys[lowerValue];
|
||||
if (key) {
|
||||
if (op.sep === ' ' && +op.arg > 0) {
|
||||
// {TAB 3}
|
||||
op.type = 'group';
|
||||
op.value = [];
|
||||
var count = +op.arg;
|
||||
for (var i = 0; i < count; i++) {
|
||||
op.value.push({type: 'key', value: key});
|
||||
}
|
||||
} else {
|
||||
// {TAB}
|
||||
op.type = 'key';
|
||||
op.value = key;
|
||||
}
|
||||
return;
|
||||
}
|
||||
var substitution = AutoTypeRunner.Substitutions[lowerValue];
|
||||
if (substitution) {
|
||||
// {title}
|
||||
op.type = 'text';
|
||||
op.value = substitution(this, op);
|
||||
if (op.value === AutoTypeRunner.PendingResolve) {
|
||||
this.pendingResolvesCount++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.tryParseCommand(op)) {
|
||||
throw 'Bad op: ' + op.value;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.tryParseCommand = function(op) {
|
||||
switch (op.value.toLowerCase()) {
|
||||
case 'clearfield':
|
||||
// {CLEARFIELD}
|
||||
op.type = 'group';
|
||||
op.value = [
|
||||
{ type: 'key', value: 'end' },
|
||||
{ type: 'key', value: 'home', mod: { '+': true } },
|
||||
{ type: 'key', value: 'bs' }
|
||||
];
|
||||
return true;
|
||||
case 'vkey':
|
||||
// {VKEY 10} {VKEY 0x1F}
|
||||
op.type = 'key';
|
||||
op.value = parseInt(op.arg);
|
||||
if (isNaN(op.value) || op.value <= 0) {
|
||||
throw 'Bad vkey: ' + op.arg;
|
||||
}
|
||||
return true;
|
||||
case 'delay':
|
||||
// {DELAY 5} {DELAY=5}
|
||||
op.type = 'cmd';
|
||||
op.value = op.sep === '=' ? 'setDelay' : 'wait';
|
||||
if (!op.arg) {
|
||||
throw 'Delay requires milliseconds count';
|
||||
}
|
||||
if (isNaN(+op.arg)) {
|
||||
throw 'Bad delay: ' + op.arg;
|
||||
}
|
||||
if (op.arg < 0) {
|
||||
throw 'Delay requires positive interval';
|
||||
}
|
||||
op.arg = +op.arg;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
if (!field || !this.entry) {
|
||||
return '';
|
||||
}
|
||||
field = field.toLowerCase();
|
||||
var value = null;
|
||||
_.findKey(this.entry.entry.fields, function(val, f) {
|
||||
if (f.toLowerCase() === field) {
|
||||
value = val;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
if (value.isProtected) {
|
||||
op.type = 'group';
|
||||
var ops = [];
|
||||
value.forEachChar(function(ch) {
|
||||
if (ch === 10 || ch === 13) {
|
||||
ops.push({type: 'key', value: 'enter'});
|
||||
} else {
|
||||
ops.push({type: 'text', value: String.fromCharCode(ch)});
|
||||
}
|
||||
});
|
||||
return ops;
|
||||
} else {
|
||||
var parts = value.split(/[\r\n]/g);
|
||||
if (parts.length === 1) {
|
||||
return value;
|
||||
}
|
||||
op.type = 'group';
|
||||
var partsOps = [];
|
||||
parts.forEach(function(part) {
|
||||
if (partsOps.length) {
|
||||
partsOps.push({type: 'key', value: 'enter'});
|
||||
}
|
||||
if (part) {
|
||||
partsOps.push({type: 'text', value: part});
|
||||
}
|
||||
});
|
||||
return partsOps;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getEntryGroupName = function() {
|
||||
return this.entry && this.entry.group.get('title');
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.dt = function(part) {
|
||||
switch (part) {
|
||||
case 'simple':
|
||||
return this.dt('Y') + this.dt('M') + this.dt('D') + this.dt('h') + this.dt('m') + this.dt('s');
|
||||
case 'Y':
|
||||
return this.now.getFullYear().toString();
|
||||
case 'M':
|
||||
return Format.pad(this.now.getMonth() + 1, 2);
|
||||
case 'D':
|
||||
return Format.pad(this.now.getDate(), 2);
|
||||
case 'h':
|
||||
return Format.pad(this.now.getHours(), 2);
|
||||
case 'm':
|
||||
return Format.pad(this.now.getMinutes(), 2);
|
||||
case 's':
|
||||
return Format.pad(this.now.getSeconds(), 2);
|
||||
default:
|
||||
throw 'Bad part: ' + part;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.udt = function(part) {
|
||||
switch (part) {
|
||||
case 'simple':
|
||||
return this.udt('Y') + this.udt('M') + this.udt('D') + this.udt('h') + this.udt('m') + this.udt('s');
|
||||
case 'Y':
|
||||
return this.now.getUTCFullYear().toString();
|
||||
case 'M':
|
||||
return Format.pad(this.now.getUTCMonth() + 1, 2);
|
||||
case 'D':
|
||||
return Format.pad(this.now.getUTCDate(), 2);
|
||||
case 'h':
|
||||
return Format.pad(this.now.getUTCHours(), 2);
|
||||
case 'm':
|
||||
return Format.pad(this.now.getUTCMinutes(), 2);
|
||||
case 's':
|
||||
return Format.pad(this.now.getUTCSeconds(), 2);
|
||||
default:
|
||||
throw 'Bad part: ' + part;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getOtp = function(op) {
|
||||
if (!this.entry) {
|
||||
return '';
|
||||
}
|
||||
this.entry.initOtpGenerator();
|
||||
if (!this.entry.otpGenerator) {
|
||||
return '';
|
||||
}
|
||||
var that = this;
|
||||
this.entry.otpGenerator.next(function(otp) {
|
||||
that.pendingResolved(op, otp, otp ? undefined : 'OTP error');
|
||||
});
|
||||
return AutoTypeRunner.PendingResolve;
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.pendingResolved = function(op, value, error) {
|
||||
var wasPending = op.value === AutoTypeRunner.PendingResolve;
|
||||
if (value) {
|
||||
op.value = value;
|
||||
}
|
||||
if (!wasPending) {
|
||||
return;
|
||||
}
|
||||
this.pendingResolvesCount--;
|
||||
if ((this.pendingResolvesCount === 0 || error) && this.resolveCallback) {
|
||||
this.resolveCallback(error);
|
||||
this.resolveCallback = null;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscate = function() {
|
||||
this.obfuscateOps(this.ops);
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscateOps = function(ops) {
|
||||
for (var i = 0, len = ops.length; i < len; i++) {
|
||||
var op = ops[i];
|
||||
if (op.mod) {
|
||||
continue;
|
||||
}
|
||||
if (op.type === 'text') {
|
||||
this.obfuscateOp(op);
|
||||
} else if (op.type === 'group') {
|
||||
var onlyText = op.value.every(function(grOp) { return grOp.type === 'text' && !grOp.mod; });
|
||||
if (onlyText) {
|
||||
this.obfuscateOp(op);
|
||||
} else {
|
||||
this.obfuscateOps(op.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscateOp = function(op) {
|
||||
var letters = [];
|
||||
if (op.type === 'text') {
|
||||
if (!op.value || op.value.length <= 1) {
|
||||
return;
|
||||
}
|
||||
letters = op.value.split('');
|
||||
} else {
|
||||
op.value.forEach(function(grOp) { letters.push.apply(letters, grOp.value.split('')); });
|
||||
}
|
||||
if (letters.length <= 1) {
|
||||
return;
|
||||
}
|
||||
var obfuscator = new AutoTypeObfuscator(letters);
|
||||
op.value = obfuscator.obfuscate();
|
||||
op.type = 'group';
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.run = function(callback) {
|
||||
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this));
|
||||
this.emitterState = {
|
||||
callback: callback,
|
||||
stack: [],
|
||||
ops: this.ops,
|
||||
opIx: 0,
|
||||
mod: {},
|
||||
activeMod: {},
|
||||
finished: null
|
||||
};
|
||||
this.emitNext();
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
if (err) {
|
||||
this.emitterState.finished = true;
|
||||
this.emitterState.callback(err);
|
||||
return;
|
||||
}
|
||||
if (this.emitterState.finished) {
|
||||
this.emitterState.callback();
|
||||
return;
|
||||
}
|
||||
this.resetEmitterMod(this.emitterState.mod);
|
||||
if (this.emitterState.opIx >= this.emitterState.ops.length) {
|
||||
var state = this.emitterState.stack.pop();
|
||||
if (state) {
|
||||
_.extend(this.emitterState, { ops: state.ops, opIx: state.opIx, mod: state.mod });
|
||||
this.emitNext();
|
||||
} else {
|
||||
this.resetEmitterMod({});
|
||||
this.emitterState.finished = true;
|
||||
emitterLogger.debug('waitComplete');
|
||||
this.emitter.waitComplete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
var op = this.emitterState.ops[this.emitterState.opIx];
|
||||
if (op.type === 'group') {
|
||||
if (op.mod) {
|
||||
this.setEmitterMod(op.mod);
|
||||
}
|
||||
this.emitterState.stack.push({
|
||||
ops: this.emitterState.ops,
|
||||
opIx: this.emitterState.opIx + 1,
|
||||
mod: _.clone(this.emitterState.mod)
|
||||
});
|
||||
_.extend(this.emitterState, {
|
||||
ops: op.value,
|
||||
opIx: 0,
|
||||
mod: _.clone(this.emitterState.activeMod)
|
||||
});
|
||||
this.emitNext();
|
||||
return;
|
||||
}
|
||||
this.emitterState.opIx++;
|
||||
if (op.mod) {
|
||||
this.setEmitterMod(op.mod);
|
||||
}
|
||||
switch (op.type) {
|
||||
case 'text':
|
||||
emitterLogger.debug('text', op.value);
|
||||
if (op.value) {
|
||||
this.emitter.text(op.value);
|
||||
}
|
||||
break;
|
||||
case 'key':
|
||||
emitterLogger.debug('key', op.value);
|
||||
this.emitter.key(op.value);
|
||||
break;
|
||||
case 'cmd':
|
||||
var method = this.emitter[op.value];
|
||||
if (!method) {
|
||||
throw 'Bad cmd: ' + op.value;
|
||||
}
|
||||
emitterLogger.debug(op.value, op.arg);
|
||||
method.call(this.emitter, op.arg);
|
||||
break;
|
||||
default:
|
||||
throw 'Bad op: ' + op.type;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.setEmitterMod = function(addedMod) {
|
||||
Object.keys(addedMod).forEach(function(mod) {
|
||||
if (addedMod[mod] && !this.emitterState.activeMod[mod]) {
|
||||
emitterLogger.debug('mod', mod, true);
|
||||
this.emitter.setMod(mod, true);
|
||||
this.emitterState.activeMod[mod] = true;
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resetEmitterMod = function(targetState) {
|
||||
Object.keys(this.emitterState.activeMod).forEach(function(mod) {
|
||||
if (this.emitterState.activeMod[mod] && !targetState[mod]) {
|
||||
emitterLogger.debug('mod', mod, false);
|
||||
this.emitter.setMod(mod, false);
|
||||
delete this.emitterState.activeMod[mod];
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
module.exports = AutoTypeRunner;
|
|
@ -0,0 +1,106 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../../comp/launcher');
|
||||
|
||||
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
|
||||
var KeyMap = {
|
||||
tab: 48, enter: 36, space: 49,
|
||||
up: 126, down: 125, left: 123, right: 124, home: 115, end: 119, pgup: 116, pgdn: 121,
|
||||
ins: 114, del: 117, bs: 51, esc: 53,
|
||||
win: 55, rwin: 55,
|
||||
f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97, f7: 98, f8: 100, f9: 101,
|
||||
f10: 109, f11: 103, f12: 111, f13: 105, f14: 107, f15: 113, f16: 106,
|
||||
add: 69, subtract: 78, multiply: 67, divide: 75,
|
||||
n0: 82, n1: 83, n2: 84, n3: 85, n4: 86,
|
||||
n5: 87, n6: 88, n7: 89, n8: 91, n9: 92
|
||||
};
|
||||
|
||||
var ModMap = {
|
||||
'^': 'command',
|
||||
'+': 'shift',
|
||||
'%': 'option',
|
||||
'^^': 'control'
|
||||
};
|
||||
|
||||
var AutoTypeEmitter = function(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[mod] = true;
|
||||
} else {
|
||||
delete this.mod[mod];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function(text) {
|
||||
text = text.replace(/"/g, '\\"').replace(/\\/g, '\\\\');
|
||||
this.pendingScript.push('keystroke "' + text + '"'+ this.modString());
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function(key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key code ' + key + this.modString());
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function(text) {
|
||||
this.pendingScript.push('delay 0.5');
|
||||
this.pendingScript.push('set the clipboard to "' + text.replace(/"/g, '\\"') + '"');
|
||||
this.pendingScript.push('delay 0.5');
|
||||
this.pendingScript.push('keystroke "v" using command down');
|
||||
this.pendingScript.push('delay 0.5');
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function(time) {
|
||||
this.pendingScript.push('delay ' + (time / 1000));
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function() {
|
||||
if (this.pendingScript.length) {
|
||||
var script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function(delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function() {
|
||||
var keys = Object.keys(this.mod);
|
||||
if (!keys.length) {
|
||||
return '';
|
||||
}
|
||||
return ' using {' + keys.map(AutoTypeEmitter.prototype.mapMod).join(', ') + '}';
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.mapMod = function(mod) {
|
||||
return ModMap[mod] + ' down';
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function(script) {
|
||||
script = 'tell application "System Events" \n' + script + '\nend tell';
|
||||
Launcher.spawn({
|
||||
cmd: 'osascript',
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitter;
|
|
@ -0,0 +1,100 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../../comp/launcher');
|
||||
|
||||
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
|
||||
var KeyMap = {
|
||||
tab: 'Tab', enter: 'KP_Enter', space: 'KP_Space',
|
||||
up: 'Up', down: 'Down', left: 'Left', right: 'Right', home: 'Home', end: 'End', pgup: 'Page_Up', pgdn: 'Page_Down',
|
||||
ins: 'Insert', del: 'Delete', bs: 'BackSpace', esc: 'Escape',
|
||||
win: 'Meta_L', rwin: 'Meta_R',
|
||||
f1: 'F1', f2: 'F2', f3: 'F3', f4: 'F4', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
|
||||
f10: 'F10', f11: 'F11', f12: 'F12', f13: 'F13', f14: 'F14', f15: 'F15', f16: 'F16',
|
||||
add: 'KP_Add', subtract: 'KP_Subtract', multiply: 'KP_Multiply', divide: 'KP_Divide',
|
||||
n0: 'KP_0', n1: 'KP_1', n2: 'KP_2', n3: 'KP_3', n4: 'KP_4',
|
||||
n5: 'KP_5', n6: 'KP_6', n7: 'KP_7', n8: 'KP_8', n9: 'KP_9'
|
||||
};
|
||||
|
||||
var ModMap = {
|
||||
'^': 'ctrl',
|
||||
'+': 'shift',
|
||||
'%': 'alt',
|
||||
'^^': 'ctrl'
|
||||
};
|
||||
|
||||
var AutoTypeEmitter = function(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function(text) {
|
||||
Object.keys(this.mod).forEach(function (mod) {
|
||||
this.pendingScript.push('keydown ' + ModMap[mod]);
|
||||
});
|
||||
this.pendingScript.push('type ' + text);
|
||||
var that = this;
|
||||
this.waitComplete(function(err) {
|
||||
if (err) { return that.callback(err); }
|
||||
Object.keys(that.mod).forEach(function (mod) {
|
||||
that.pendingScript.push('keyup ' + ModMap[mod]);
|
||||
});
|
||||
that.callback();
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function(key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key].toString(16);
|
||||
}
|
||||
this.pendingScript.push('key --clearmodifiers ' + this.modString() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function(text) {
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
Launcher.setClipboardText(text);
|
||||
this.pendingScript.push('key --clearmodifiers shift+Insert');
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function(callback) {
|
||||
if (this.pendingScript.length) {
|
||||
var script = this.pendingScript.join(' ');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script, callback);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function() {
|
||||
var mod = '';
|
||||
Object.keys(this.mod).forEach(function (key) {
|
||||
mod += key + '+';
|
||||
});
|
||||
return mod;
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function(script, callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['-'],
|
||||
data: script,
|
||||
complete: callback || this.callback
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitter;
|
|
@ -0,0 +1,106 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../../comp/launcher'),
|
||||
AutoTypeHelper = require('../helper/auto-type-helper-win32');
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/system.windows.forms.sendkeys.send(v=vs.110).aspx
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
var KeyMap = {
|
||||
tab: '{tab}', enter: '{enter}', space: '{space}',
|
||||
up: '{up}', down: '{down}', left: '{left}', right: '{right}', home: '{home}', end: '{end}', pgup: '{pgup}', pgdn: '{pgdn}',
|
||||
ins: '{ins}', del: '{del}', bs: '{bs}', esc: '{esc}',
|
||||
win: 0x5B, rwin: 0x5C,
|
||||
f1: '{f1}', f2: '{f2}', f3: '{f3}', f4: '{f4}', f5: '{f5}', f6: '{f6}', f7: '{f7}', f8: '{f8}', f9: '{f9}',
|
||||
f10: '{f10}', f11: '{f11}', f12: '{f12}', f13: '{f13}', f14: '{f14}', f15: '{f15}', f16: '{f16}',
|
||||
add: '{add}', subtract: '{subtract}', multiply: '{multiply}', divide: '{divide}',
|
||||
n0: '0', n1: '1', n2: '2', n3: '3', n4: '4',
|
||||
n5: '5', n6: '6', n7: '7', n8: '8', n9: '9'
|
||||
};
|
||||
|
||||
var ModMap = {
|
||||
'^': '^',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
var TextReplaceRegex = /[\(\)\{}\[\]\+\^%~]/g;
|
||||
|
||||
var AutoTypeEmitter = function(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function(text) {
|
||||
text = this.addMod(text.replace(TextReplaceRegex, function(match) { return '{' + match + '}'; }));
|
||||
this.pendingScript.push('text ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function(key) {
|
||||
if (typeof key !== 'number') {
|
||||
key = KeyMap[key];
|
||||
if (!key) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
}
|
||||
if (typeof key === 'number') {
|
||||
this.pendingScript.push('key ' + this.addMod('') + key);
|
||||
} else {
|
||||
var text = this.addMod(key);
|
||||
this.pendingScript.push('text ' + text);
|
||||
}
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function(text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function(time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function() {
|
||||
if (this.pendingScript.length) {
|
||||
var script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function(delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.addMod = function(text) {
|
||||
var keys = Object.keys(this.mod);
|
||||
if (!keys.length || !text) {
|
||||
return text;
|
||||
}
|
||||
return keys.join('') + (text.length > 1 ? '(' + text + ')' : text);
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function(script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitter;
|
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../../comp/launcher');
|
||||
|
||||
var ForeMostAppScript = 'tell application "System Events" to set frontApp to name of first process whose frontmost is true';
|
||||
var ChromeScript = 'tell application "{}" to set appUrl to URL of active tab of front window\n' +
|
||||
'tell application "{}" to set appTitle to title of active tab of front window\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
var SafariScript = 'tell application "{}" to set appUrl to URL of front document\n' +
|
||||
'tell application "{}" to set appTitle to name of front document\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
var OtherAppsScript = 'tell application "System Events"\n' +
|
||||
' tell process "{}"\n' +
|
||||
' tell (1st window whose value of attribute "AXMain" is true)\n' +
|
||||
' set windowTitle to value of attribute "AXTitle"\n' +
|
||||
' end tell\n' +
|
||||
' end tell\n' +
|
||||
'end tell';
|
||||
|
||||
var AutoTypeHelper = function() {
|
||||
};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
AutoTypeHelper.exec(ForeMostAppScript, function(err, out) {
|
||||
if (err) { return callback(err); }
|
||||
var appName = out.trim();
|
||||
// getting urls and titles from Chrome or Safari:
|
||||
// - will suit in 90% cases
|
||||
// - does not require assistive access
|
||||
// - allows to get url
|
||||
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), function(err, out) {
|
||||
if (err) { return callback(err); }
|
||||
var parts = out.split('\n');
|
||||
return callback(null, parts[1].trim(), parts[0].trim());
|
||||
});
|
||||
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), function(err, out) {
|
||||
if (err) { return callback(err); }
|
||||
var parts = out.split('\n');
|
||||
return callback(null, parts[1].trim(), parts[0].trim());
|
||||
});
|
||||
} else {
|
||||
// special cases are not available. this method may ask the user about assistive access
|
||||
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), function(err, out) {
|
||||
if (err) { return callback(err); }
|
||||
return callback(null, out.trim());
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeHelper.exec = function(script, callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'osascript',
|
||||
args: ['-e', script],
|
||||
complete: callback
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelper;
|
|
@ -0,0 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../../comp/launcher');
|
||||
|
||||
var AutoTypeHelper = function() {
|
||||
};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['getactivewindow', 'getwindowname'],
|
||||
callback: function(err, res) {
|
||||
return callback(err, res ? res.trim() : undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelper;
|
|
@ -0,0 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
var Launcher = require('../../comp/launcher');
|
||||
|
||||
var AutoTypeHelper = function() {
|
||||
};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeHelper.getHelperPath(),
|
||||
args: ['--window-info'],
|
||||
complete: function(err, out) {
|
||||
if (err) { return callback(err); }
|
||||
var parts = out.split('\n');
|
||||
return callback(null, (parts[0] || '').trim(),
|
||||
parts[1] ? parts[1].trim() : undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeHelper.getHelperPath = function() {
|
||||
return Launcher.getAppPath('KeeWebHelper.exe');
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelper;
|
|
@ -0,0 +1,107 @@
|
|||
'use strict';
|
||||
|
||||
var AutoTypeParser = require('./auto-type-parser'),
|
||||
AutoTypeHelperFactory = require('./auto-type-helper-factory'),
|
||||
Launcher = require('../comp/launcher'),
|
||||
Logger = require('../util/logger'),
|
||||
Timeouts = require('../const/timeouts');
|
||||
|
||||
var logger = new Logger('auto-type');
|
||||
var clearTextAutoTypeLog = localStorage.autoTypeDebug;
|
||||
|
||||
var AutoType = {
|
||||
helper: AutoTypeHelperFactory.create(),
|
||||
|
||||
enabled: !!Launcher,
|
||||
|
||||
run: function(entry, callback) {
|
||||
var sequence = entry.getEffectiveAutoTypeSeq();
|
||||
logger.debug('Start', sequence);
|
||||
var that = this;
|
||||
var ts = logger.ts();
|
||||
try {
|
||||
var parser = new AutoTypeParser(sequence);
|
||||
var runner = parser.parse();
|
||||
logger.debug('Parsed', that.printOps(runner.ops));
|
||||
runner.resolve(entry, function(err) {
|
||||
if (err) {
|
||||
logger.error('Resolve error', err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
logger.debug('Resolved', that.printOps(runner.ops));
|
||||
if (entry.autoTypeObfuscation) {
|
||||
try {
|
||||
runner.obfuscate();
|
||||
} catch (e) {
|
||||
logger.error('Obfuscate error', e);
|
||||
return callback && callback(e);
|
||||
}
|
||||
logger.debug('Obfuscated');
|
||||
}
|
||||
runner.run(function(err) {
|
||||
if (err) {
|
||||
logger.error('Run error', err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
logger.debug('Complete', logger.ts(ts));
|
||||
return callback && callback();
|
||||
});
|
||||
});
|
||||
} catch (ex) {
|
||||
logger.error('Parse error', ex);
|
||||
return callback && callback(ex);
|
||||
}
|
||||
},
|
||||
|
||||
validate: function(entry, sequence, callback) {
|
||||
try {
|
||||
var parser = new AutoTypeParser(sequence);
|
||||
var runner = parser.parse();
|
||||
runner.resolve(entry, callback);
|
||||
} catch (ex) {
|
||||
return callback(ex);
|
||||
}
|
||||
},
|
||||
|
||||
printOps: function(ops) {
|
||||
return '[' + ops.map(this.printOp, this).join(',') + ']';
|
||||
},
|
||||
|
||||
printOp: function(op) {
|
||||
var mod = op.mod ? Object.keys(op.mod).join('') : '';
|
||||
if (op.type === 'group') {
|
||||
return mod + this.printOps(op.value);
|
||||
}
|
||||
if (op.type === 'text') {
|
||||
var value = op.value;
|
||||
if (!clearTextAutoTypeLog) {
|
||||
value = value.replace(/./g, '*');
|
||||
}
|
||||
return mod + value;
|
||||
}
|
||||
return mod + op.type + ':' + op.value;
|
||||
},
|
||||
|
||||
hideWindow: function(callback) {
|
||||
logger.debug('Hide window');
|
||||
if (Launcher.hideWindowIfActive()) {
|
||||
setTimeout(callback, Timeouts.AutoTypeAfterHide);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
getActiveWindowTitle: function(callback) {
|
||||
logger.debug('Get window title');
|
||||
return this.helper.getActiveWindowTitle(function(err, title, url) {
|
||||
if (err) {
|
||||
logger.error('Error get window title', err);
|
||||
} else {
|
||||
logger.debug('Window title', title, url);
|
||||
}
|
||||
return callback(err, title);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoType;
|
|
@ -20,10 +20,6 @@ var FileCollection = Backbone.Collection.extend({
|
|||
|
||||
getByName: function(name) {
|
||||
return this.find(function(file) { return file.get('name').toLowerCase() === name.toLowerCase(); });
|
||||
},
|
||||
|
||||
getById: function(id) {
|
||||
return this.find(function(file) { return file.get('id') === id; });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -263,6 +263,17 @@ var DropboxLink = {
|
|||
this._getClient(function(err) { complete(err); }, overrideAppKey);
|
||||
},
|
||||
|
||||
logout: function() {
|
||||
if (this._dropboxClient) {
|
||||
try {
|
||||
this._dropboxClient.signOut();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
this._dropboxClient.reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
resetClient: function() {
|
||||
this._dropboxClient = null;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
var AppSettingsModel = require('../models/app-settings-model');
|
||||
|
||||
var ExportApi = {
|
||||
settings: {
|
||||
get: function(key) { return key ? AppSettingsModel.instance.get(key) : AppSettingsModel.instance.toJSON(); },
|
||||
set: function(key, value) { AppSettingsModel.instance.set(key, value); }
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ExportApi;
|
|
@ -1,28 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone'),
|
||||
Locale = require('../util/locale');
|
||||
Locale = require('../util/locale'),
|
||||
Logger = require('../util/logger');
|
||||
|
||||
var Launcher;
|
||||
|
||||
var logger = new Logger('launcher');
|
||||
|
||||
if (window.process && window.process.versions && window.process.versions.electron) {
|
||||
/* jshint node:true */
|
||||
Launcher = {
|
||||
name: 'electron',
|
||||
version: window.process.versions.electron,
|
||||
req: window.require,
|
||||
electron: function() {
|
||||
return this.req('electron');
|
||||
},
|
||||
remoteApp: function() {
|
||||
return this.electron().remote.app;
|
||||
},
|
||||
remReq: function(mod) {
|
||||
return this.req('remote').require(mod);
|
||||
return this.electron().remote.require(mod);
|
||||
},
|
||||
openLink: function(href) {
|
||||
this.req('shell').openExternal(href);
|
||||
this.electron().shell.openExternal(href);
|
||||
},
|
||||
devTools: true,
|
||||
openDevTools: function() {
|
||||
this.req('remote').getCurrentWindow().openDevTools();
|
||||
this.electron().remote.getCurrentWindow().openDevTools();
|
||||
},
|
||||
getSaveFileName: function(defaultPath, cb) {
|
||||
if (defaultPath) {
|
||||
var homePath = this.remReq('app').getPath('userDesktop');
|
||||
var homePath = this.remReq('electron').app.getPath('userDesktop');
|
||||
defaultPath = this.req('path').join(homePath, defaultPath);
|
||||
}
|
||||
this.remReq('dialog').showSaveDialog({
|
||||
|
@ -32,10 +42,13 @@ if (window.process && window.process.versions && window.process.versions.electro
|
|||
}, cb);
|
||||
},
|
||||
getUserDataPath: function(fileName) {
|
||||
return this.req('path').join(this.remReq('app').getPath('userData'), fileName || '');
|
||||
return this.req('path').join(this.remoteApp().getPath('userData'), fileName || '');
|
||||
},
|
||||
getTempPath: function(fileName) {
|
||||
return this.req('path').join(this.remReq('app').getPath('temp'), fileName || '');
|
||||
return this.req('path').join(this.remoteApp().getPath('temp'), fileName || '');
|
||||
},
|
||||
getAppPath: function(fileName) {
|
||||
return this.req('path').join(__dirname, fileName || '');
|
||||
},
|
||||
writeFile: function(path, data) {
|
||||
this.req('fs').writeFileSync(path, new window.Buffer(data));
|
||||
|
@ -69,7 +82,7 @@ if (window.process && window.process.versions && window.process.versions.electro
|
|||
this.requestExit();
|
||||
},
|
||||
requestExit: function() {
|
||||
var app = this.remReq('app');
|
||||
var app = this.remoteApp();
|
||||
if (this.restartPending) {
|
||||
app.restartApp();
|
||||
} else {
|
||||
|
@ -84,25 +97,25 @@ if (window.process && window.process.versions && window.process.versions.electro
|
|||
this.restartPending = false;
|
||||
},
|
||||
setClipboardText: function(text) {
|
||||
return this.req('clipboard').writeText(text);
|
||||
return this.electron().clipboard.writeText(text);
|
||||
},
|
||||
getClipboardText: function() {
|
||||
return this.req('clipboard').readText();
|
||||
return this.electron().clipboard.readText();
|
||||
},
|
||||
clearClipboardText: function() {
|
||||
return this.req('clipboard').clear();
|
||||
return this.electron().clipboard.clear();
|
||||
},
|
||||
minimizeApp: function() {
|
||||
this.remReq('app').minimizeApp();
|
||||
this.remoteApp().minimizeApp();
|
||||
},
|
||||
canMinimize: function() {
|
||||
return process.platform !== 'darwin';
|
||||
},
|
||||
updaterEnabled: function() {
|
||||
return this.req('remote').process.argv.indexOf('--disable-updater') === -1;
|
||||
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
|
||||
},
|
||||
resolveProxy: function(url, callback) {
|
||||
var window = this.remReq('app').getMainWindow();
|
||||
var window = this.remoteApp().getMainWindow();
|
||||
var session = window.webContents.session;
|
||||
session.resolveProxy(url, function(proxy) {
|
||||
var match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
|
||||
|
@ -111,7 +124,60 @@ if (window.process && window.process.versions && window.process.versions.electro
|
|||
});
|
||||
},
|
||||
openWindow: function(opts) {
|
||||
return this.remReq('app').openWindow(opts);
|
||||
return this.remoteApp().openWindow(opts);
|
||||
},
|
||||
hideWindowIfActive: function() {
|
||||
var app = this.remoteApp();
|
||||
var win = app.getMainWindow();
|
||||
var visible = win.isVisible(), focused = win.isFocused();
|
||||
if (!visible || !focused) {
|
||||
return false;
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
app.hide();
|
||||
} else {
|
||||
win.minimize();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
spawn: function(config) {
|
||||
var ts = logger.ts();
|
||||
var complete = config.complete;
|
||||
var ps = this.req('child_process').spawn(config.cmd, config.args);
|
||||
[ps.stdin, ps.stdout, ps.stderr].forEach(function(s) { s.setEncoding('utf-8'); });
|
||||
var stderr = '';
|
||||
var stdout = '';
|
||||
ps.stderr.on('data', function(d) { stderr += d.toString('utf-8'); });
|
||||
ps.stdout.on('data', function(d) { stdout += d.toString('utf-8'); });
|
||||
ps.on('close', function(code) {
|
||||
stdout = stdout.trim();
|
||||
stderr = stderr.trim();
|
||||
var msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
|
||||
if (code) {
|
||||
logger.error(msg + '\n' + stdout + '\n' + stderr);
|
||||
} else {
|
||||
logger.info(msg + (stdout ? '\n' + stdout : ''));
|
||||
}
|
||||
if (complete) {
|
||||
complete(code ? 'Exit code ' + code : null, stdout, code);
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
ps.on('error', function(err) {
|
||||
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), err);
|
||||
if (complete) {
|
||||
complete(err);
|
||||
complete = null;
|
||||
}
|
||||
});
|
||||
if (config.data) {
|
||||
ps.stdin.write(config.data);
|
||||
ps.stdin.end();
|
||||
}
|
||||
return ps;
|
||||
},
|
||||
platform: function() {
|
||||
return process.platform;
|
||||
}
|
||||
};
|
||||
Backbone.on('launcher-exit-request', function() {
|
||||
|
|
|
@ -5,7 +5,7 @@ var kdbxweb = require('kdbxweb');
|
|||
var SecureInput = function() {
|
||||
this.el = null;
|
||||
this.minChar = 0x1400 + Math.round(Math.random() * 100);
|
||||
this.maxLen = 128;
|
||||
this.maxLen = 1024;
|
||||
this.length = 0;
|
||||
this.pseudoValue = '';
|
||||
this.salt = new Uint32Array(0);
|
||||
|
|
|
@ -38,10 +38,7 @@ var Updater = {
|
|||
},
|
||||
|
||||
init: function() {
|
||||
var willCheckNow = this.scheduleNextCheck();
|
||||
if (!willCheckNow && this.getAutoUpdateType()) {
|
||||
this.check();
|
||||
}
|
||||
this.scheduleNextCheck();
|
||||
if (!Launcher && window.applicationCache) {
|
||||
window.applicationCache.addEventListener('updateready', this.checkAppCacheUpdateReady.bind(this));
|
||||
this.checkAppCacheUpdateReady();
|
||||
|
@ -63,7 +60,6 @@ var Updater = {
|
|||
}
|
||||
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
|
||||
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
|
||||
return timeDiff === this.MinUpdateTimeout;
|
||||
},
|
||||
|
||||
check: function(startedByUser) {
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
var Links = {
|
||||
Repo: 'https://github.com/antelle/keeweb',
|
||||
Desktop: 'https://github.com/antelle/keeweb/releases/latest',
|
||||
Repo: 'https://github.com/keeweb/keeweb',
|
||||
Desktop: 'https://github.com/keeweb/keeweb/releases/latest',
|
||||
WebApp: 'https://app.keeweb.info',
|
||||
BetaWebApp: 'https://beta.keeweb.info',
|
||||
License: 'https://github.com/antelle/keeweb/blob/master/LICENSE.txt',
|
||||
License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE.txt',
|
||||
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
|
||||
UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
|
||||
ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes',
|
||||
SelfHostedDropbox: 'https://github.com/antelle/keeweb#self-hosting',
|
||||
Manifest: 'https://antelle.github.io/keeweb/manifest.appcache'
|
||||
UpdateDesktop: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
|
||||
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
|
||||
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
|
||||
Manifest: 'https://app.keeweb.info/manifest.appcache',
|
||||
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type'
|
||||
};
|
||||
|
||||
module.exports = Links;
|
||||
|
|
|
@ -7,7 +7,9 @@ var Timeouts = {
|
|||
FileChangeSync: 3000,
|
||||
BeforeAutoLock: 300,
|
||||
CheckWindowClosed: 300,
|
||||
OtpFadeDuration: 10000
|
||||
OtpFadeDuration: 10000,
|
||||
AutoTypeAfterHide: 100,
|
||||
DrobDownClickWait: 500
|
||||
};
|
||||
|
||||
module.exports = Timeouts;
|
||||
|
|
|
@ -36,10 +36,12 @@ var Scrollable = {
|
|||
if (this.scroll) {
|
||||
this.scroll.update();
|
||||
this.requestAnimationFrame(function() {
|
||||
this.scroll.update();
|
||||
var barHeight = this.scrollerBar.height(),
|
||||
wrapperHeight = this.scrollerBarWrapper.height();
|
||||
this.scrollerBarWrapper.toggleClass('invisible', barHeight >= wrapperHeight);
|
||||
if (this.scroll) {
|
||||
this.scroll.update();
|
||||
var barHeight = this.scrollerBar.height(),
|
||||
wrapperHeight = this.scrollerBarWrapper.height();
|
||||
this.scrollerBarWrapper.toggleClass('invisible', barHeight >= wrapperHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -69,6 +69,16 @@ _.extend(Backbone.View.prototype, {
|
|||
|
||||
remove: function() {
|
||||
this.trigger('remove');
|
||||
this.removeInnerViews();
|
||||
if (this.scroll) {
|
||||
try { this.scroll.dispose(); }
|
||||
catch (e) { }
|
||||
}
|
||||
Tip.hideTips(this.$el);
|
||||
this._parentRemove(arguments);
|
||||
},
|
||||
|
||||
removeInnerViews: function() {
|
||||
if (this.views) {
|
||||
_.each(this.views, function(view) {
|
||||
if (view) {
|
||||
|
@ -81,13 +91,8 @@ _.extend(Backbone.View.prototype, {
|
|||
}
|
||||
}
|
||||
});
|
||||
this.views = {};
|
||||
}
|
||||
if (this.scroll) {
|
||||
try { this.scroll.dispose(); }
|
||||
catch (e) { }
|
||||
}
|
||||
Tip.hideTips(this.$el);
|
||||
this._parentRemove(arguments);
|
||||
},
|
||||
|
||||
deferRender: function() {
|
||||
|
|
|
@ -41,8 +41,31 @@ var AppModel = Backbone.Model.extend({
|
|||
this.appLogger = new Logger('app');
|
||||
},
|
||||
|
||||
loadConfig: function(configLocation, callback) {
|
||||
this.appLogger.debug('Loading config from', configLocation);
|
||||
var ts = this.appLogger.ts();
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', configLocation);
|
||||
xhr.responseType = 'json';
|
||||
xhr.send();
|
||||
var that = this;
|
||||
xhr.addEventListener('load', function() {
|
||||
if (!xhr.response) {
|
||||
that.appLogger.error('Error loading app config', xhr.statusText);
|
||||
return callback(true);
|
||||
}
|
||||
that.appLogger.info('Loaded app config from', configLocation, that.appLogger.ts(ts));
|
||||
that.settings.set(xhr.response);
|
||||
callback();
|
||||
});
|
||||
xhr.addEventListener('error', function() {
|
||||
that.appLogger.error('Error loading app config', xhr.statusText, xhr.status);
|
||||
callback(true);
|
||||
});
|
||||
},
|
||||
|
||||
addFile: function(file) {
|
||||
if (this.files.getById(file.id)) {
|
||||
if (this.files.get(file.id)) {
|
||||
return false;
|
||||
}
|
||||
this.files.add(file);
|
||||
|
@ -63,10 +86,7 @@ var AppModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
reloadFile: function(file) {
|
||||
this.menu.groupsSection.removeByFile(file, true);
|
||||
file.get('groups').forEach(function (group) {
|
||||
this.menu.groupsSection.addItem(group);
|
||||
}, this);
|
||||
this.menu.groupsSection.replaceByFile(file, file.get('groups').first());
|
||||
this.updateTags();
|
||||
},
|
||||
|
||||
|
@ -91,7 +111,7 @@ var AppModel = Backbone.Model.extend({
|
|||
if (this.tags.length) {
|
||||
this.menu.tagsSection.set('scrollable', true);
|
||||
this.menu.tagsSection.setItems(this.tags.map(function (tag) {
|
||||
return {title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag};
|
||||
return {title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag, editable: true};
|
||||
}));
|
||||
} else {
|
||||
this.menu.tagsSection.set('scrollable', false);
|
||||
|
@ -110,6 +130,13 @@ var AppModel = Backbone.Model.extend({
|
|||
}
|
||||
},
|
||||
|
||||
renameTag: function(from, to) {
|
||||
this.files.forEach(function(file) {
|
||||
file.renameTag(from, to);
|
||||
});
|
||||
this.updateTags();
|
||||
},
|
||||
|
||||
closeAllFiles: function() {
|
||||
var that = this;
|
||||
this.files.each(function(file) {
|
||||
|
@ -133,7 +160,7 @@ var AppModel = Backbone.Model.extend({
|
|||
this.updateTags();
|
||||
this.menu.groupsSection.removeByFile(file);
|
||||
this.menu.filesSection.removeByFile(file);
|
||||
this.refresh();
|
||||
this.menu.select({ item: this.menu.allItemsSection.get('items').first() });
|
||||
},
|
||||
|
||||
emptyTrash: function() {
|
||||
|
@ -256,7 +283,7 @@ var AppModel = Backbone.Model.extend({
|
|||
createDemoFile: function() {
|
||||
var that = this;
|
||||
if (!this.files.getByName('Demo')) {
|
||||
var demoFile = new FileModel();
|
||||
var demoFile = new FileModel({ id: IdGenerator.uuid() });
|
||||
demoFile.openDemo(function() {
|
||||
that.addFile(demoFile);
|
||||
});
|
||||
|
@ -274,7 +301,7 @@ var AppModel = Backbone.Model.extend({
|
|||
break;
|
||||
}
|
||||
}
|
||||
var newFile = new FileModel();
|
||||
var newFile = new FileModel({ id: IdGenerator.uuid() });
|
||||
newFile.create(name);
|
||||
this.addFile(newFile);
|
||||
},
|
||||
|
@ -298,7 +325,8 @@ var AppModel = Backbone.Model.extend({
|
|||
}, fileInfo);
|
||||
} else if (params.fileData) {
|
||||
logger.info('Open file from supplied content');
|
||||
this.openFileWithData(params, callback, fileInfo, params.fileData, true);
|
||||
var needSaveToCache = params.storage !== 'file';
|
||||
this.openFileWithData(params, callback, fileInfo, params.fileData, needSaveToCache);
|
||||
} else if (!params.storage) {
|
||||
logger.info('Open file from cache as main storage');
|
||||
this.openFileFromCache(params, callback, fileInfo);
|
||||
|
@ -323,7 +351,8 @@ var AppModel = Backbone.Model.extend({
|
|||
logger.info('Open file from content loaded from storage');
|
||||
params.fileData = data;
|
||||
params.rev = stat && stat.rev || null;
|
||||
that.openFileWithData(params, callback, fileInfo, data, true);
|
||||
var needSaveToCache = storage.name !== 'file';
|
||||
that.openFileWithData(params, callback, fileInfo, data, needSaveToCache);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -331,7 +360,7 @@ var AppModel = Backbone.Model.extend({
|
|||
if (cacheRev && storage.stat) {
|
||||
logger.info('Stat file');
|
||||
storage.stat(params.path, params.opts, function(err, stat) {
|
||||
if (fileInfo && (err || stat && stat.rev === cacheRev)) {
|
||||
if (fileInfo && storage.name !== 'file' && (err || stat && stat.rev === cacheRev)) {
|
||||
logger.info('Open file from cache because ' + (err ? 'stat error' : 'it is latest'), err);
|
||||
that.openFileFromCache(params, callback, fileInfo);
|
||||
} else if (stat) {
|
||||
|
@ -346,7 +375,7 @@ var AppModel = Backbone.Model.extend({
|
|||
storageLoad();
|
||||
}
|
||||
} else {
|
||||
logger.info('Open file from cache, after load will sync', params.storage);
|
||||
logger.info('Open file from cache, will sync after load', params.storage);
|
||||
this.openFileFromCache(params, function(err, file) {
|
||||
if (!err && file) {
|
||||
logger.info('Sync just opened file');
|
||||
|
@ -376,6 +405,7 @@ var AppModel = Backbone.Model.extend({
|
|||
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.get('keyFileHash'));
|
||||
}
|
||||
var file = new FileModel({
|
||||
id: fileInfo ? fileInfo.id : IdGenerator.uuid(),
|
||||
name: params.name,
|
||||
storage: params.storage,
|
||||
path: params.path,
|
||||
|
@ -400,18 +430,16 @@ var AppModel = Backbone.Model.extend({
|
|||
if (fileInfo) {
|
||||
file.set('syncDate', fileInfo.get('syncDate'));
|
||||
}
|
||||
var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid();
|
||||
file.set('cacheId', cacheId);
|
||||
if (updateCacheOnSuccess) {
|
||||
logger.info('Save loaded file to cache');
|
||||
Storage.cache.save(cacheId, null, params.fileData);
|
||||
Storage.cache.save(file.id, null, params.fileData);
|
||||
}
|
||||
var rev = params.rev || fileInfo && fileInfo.get('rev');
|
||||
that.setFileOpts(file, params.opts);
|
||||
that.addToLastOpenFiles(file, rev);
|
||||
that.addFile(file);
|
||||
that.fileOpened(file);
|
||||
callback(null, file);
|
||||
that.fileOpened(file);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -419,6 +447,7 @@ var AppModel = Backbone.Model.extend({
|
|||
var logger = new Logger('import', params.name);
|
||||
logger.info('File import request with supplied xml');
|
||||
var file = new FileModel({
|
||||
id: IdGenerator.uuid(),
|
||||
name: params.name,
|
||||
storage: params.storage,
|
||||
path: params.path
|
||||
|
@ -435,10 +464,10 @@ var AppModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
addToLastOpenFiles: function(file, rev) {
|
||||
this.appLogger.debug('Add last open file', file.get('cacheId'), file.get('name'), file.get('storage'), file.get('path'), rev);
|
||||
this.appLogger.debug('Add last open file', file.id, file.get('name'), file.get('storage'), file.get('path'), rev);
|
||||
var dt = new Date();
|
||||
var fileInfo = new FileInfoModel({
|
||||
id: file.get('cacheId'),
|
||||
id: file.id,
|
||||
name: file.get('name'),
|
||||
storage: file.get('storage'),
|
||||
path: file.get('path'),
|
||||
|
@ -455,7 +484,7 @@ var AppModel = Backbone.Model.extend({
|
|||
keyFileHash: file.getKeyFileHash()
|
||||
});
|
||||
}
|
||||
this.fileInfos.remove(file.get('cacheId'));
|
||||
this.fileInfos.remove(file.id);
|
||||
this.fileInfos.unshift(fileInfo);
|
||||
this.fileInfos.save();
|
||||
},
|
||||
|
@ -482,6 +511,9 @@ var AppModel = Backbone.Model.extend({
|
|||
that.syncFile(file);
|
||||
}, Timeouts.FileChangeSync));
|
||||
}
|
||||
if (file.isKeyChangePending(true)) {
|
||||
Backbone.trigger('key-change-pending', { file: file });
|
||||
}
|
||||
},
|
||||
|
||||
fileClosed: function(file) {
|
||||
|
@ -497,7 +529,7 @@ var AppModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
getFileInfo: function(file) {
|
||||
return file.get('cacheId') ? this.fileInfos.get(file.get('cacheId')) :
|
||||
return this.fileInfos.get(file.id) ||
|
||||
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path'));
|
||||
},
|
||||
|
||||
|
@ -542,7 +574,6 @@ var AppModel = Backbone.Model.extend({
|
|||
if (!err) { savedToCache = true; }
|
||||
logger.info('Sync finished', err || 'no error');
|
||||
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
|
||||
file.set('cacheId', fileInfo.id);
|
||||
fileInfo.set({
|
||||
name: file.get('name'),
|
||||
storage: storage,
|
||||
|
@ -550,8 +581,7 @@ var AppModel = Backbone.Model.extend({
|
|||
opts: that.getStoreOpts(file),
|
||||
modified: file.get('modified'),
|
||||
editState: file.getLocalEditState(),
|
||||
syncDate: file.get('syncDate'),
|
||||
cacheId: fileInfo.id
|
||||
syncDate: file.get('syncDate')
|
||||
});
|
||||
if (that.settings.get('rememberKeyFiles')) {
|
||||
fileInfo.set({
|
||||
|
@ -566,7 +596,7 @@ var AppModel = Backbone.Model.extend({
|
|||
if (callback) { callback(err); }
|
||||
};
|
||||
if (!storage) {
|
||||
if (!file.get('modified') && fileInfo.id === file.get('cacheId')) {
|
||||
if (!file.get('modified') && fileInfo.id === file.id) {
|
||||
logger.info('Local, not modified');
|
||||
return complete();
|
||||
}
|
||||
|
@ -604,7 +634,7 @@ var AppModel = Backbone.Model.extend({
|
|||
}
|
||||
file.set('syncDate', new Date());
|
||||
if (file.get('modified')) {
|
||||
logger.info('Updated sync date, saving modified file to cache and storage');
|
||||
logger.info('Updated sync date, saving modified file');
|
||||
saveToCacheAndStorage();
|
||||
} else if (file.get('dirty')) {
|
||||
logger.info('Saving not modified dirty file to cache');
|
||||
|
@ -622,11 +652,14 @@ var AppModel = Backbone.Model.extend({
|
|||
});
|
||||
};
|
||||
var saveToCacheAndStorage = function() {
|
||||
logger.info('Save to cache and storage');
|
||||
logger.info('Getting file data for saving');
|
||||
file.getData(function(data, err) {
|
||||
if (err) { return complete(err); }
|
||||
if (!file.get('dirty')) {
|
||||
logger.info('Save to storage, skip cache because not dirty');
|
||||
if (storage === 'file') {
|
||||
logger.info('Saving to file storage');
|
||||
saveToStorage(data);
|
||||
} else if (!file.get('dirty')) {
|
||||
logger.info('Saving to storage, skip cache because not dirty');
|
||||
saveToStorage(data);
|
||||
} else {
|
||||
logger.info('Saving to cache');
|
||||
|
@ -690,7 +723,7 @@ var AppModel = Backbone.Model.extend({
|
|||
}
|
||||
} else if (stat.rev === fileInfo.get('rev')) {
|
||||
if (file.get('modified')) {
|
||||
logger.info('Stat found same version, modified, saving to cache and storage');
|
||||
logger.info('Stat found same version, modified, saving');
|
||||
saveToCacheAndStorage();
|
||||
} else {
|
||||
logger.info('Stat found same version, not modified');
|
||||
|
|
|
@ -25,6 +25,7 @@ var AppSettingsModel = Backbone.Model.extend({
|
|||
hideEmptyFields: false,
|
||||
skipHttpsWarning: false,
|
||||
demoOpened: false,
|
||||
fontSize: 0,
|
||||
dropbox: true,
|
||||
webdav: true,
|
||||
gdrive: true,
|
||||
|
@ -50,9 +51,4 @@ var AppSettingsModel = Backbone.Model.extend({
|
|||
AppSettingsModel.instance = new AppSettingsModel();
|
||||
AppSettingsModel.instance.load();
|
||||
|
||||
window.kwSettings = {
|
||||
get: function(key) { return AppSettingsModel.instance.get(key); },
|
||||
set: function(key, value) { AppSettingsModel.instance.set(key, value); }
|
||||
};
|
||||
|
||||
module.exports = AppSettingsModel;
|
||||
|
|
|
@ -21,7 +21,7 @@ var EntryModel = Backbone.Model.extend({
|
|||
this.entry = entry;
|
||||
this.group = group;
|
||||
this.file = file;
|
||||
if (this.id === entry.uuid.id) {
|
||||
if (this.get('uuid') === entry.uuid.id) {
|
||||
this._checkUpdatedEntry();
|
||||
}
|
||||
this._fillByEntry();
|
||||
|
@ -29,8 +29,9 @@ var EntryModel = Backbone.Model.extend({
|
|||
|
||||
_fillByEntry: function() {
|
||||
var entry = this.entry;
|
||||
this.set({id: entry.uuid.id}, {silent: true});
|
||||
this.set({id: this.file.subId(entry.uuid.id), uuid: entry.uuid.id}, {silent: true});
|
||||
this.fileName = this.file.get('name');
|
||||
this.groupName = this.group.get('title');
|
||||
this.title = entry.fields.Title || '';
|
||||
this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
|
||||
this.notes = entry.fields.Notes || '';
|
||||
|
@ -52,12 +53,16 @@ var EntryModel = Backbone.Model.extend({
|
|||
this._buildSearchText();
|
||||
this._buildSearchTags();
|
||||
this._buildSearchColor();
|
||||
this._buildAutoType();
|
||||
},
|
||||
|
||||
_checkUpdatedEntry: function() {
|
||||
if (this.isJustCreated) {
|
||||
this.isJustCreated = false;
|
||||
}
|
||||
if (this.canBeDeleted) {
|
||||
this.canBeDeleted = false;
|
||||
}
|
||||
if (this.unsaved && +this.updated !== +this.entry.times.lastModTime) {
|
||||
this.unsaved = false;
|
||||
}
|
||||
|
@ -96,6 +101,17 @@ var EntryModel = Backbone.Model.extend({
|
|||
this.searchColor = this.color;
|
||||
},
|
||||
|
||||
_buildAutoType: function() {
|
||||
this.autoTypeEnabled = this.entry.autoType.enabled;
|
||||
this.autoTypeObfuscation = this.entry.autoType.obfuscation === kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard;
|
||||
this.autoTypeSequence = this.entry.autoType.defaultSequence;
|
||||
this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem);
|
||||
},
|
||||
|
||||
_convertAutoTypeItem: function(item) {
|
||||
return { window: item.window, sequence: item.keystrokeSequence };
|
||||
},
|
||||
|
||||
_iconFromId: function(id) {
|
||||
return IconMap[id];
|
||||
},
|
||||
|
@ -140,6 +156,15 @@ var EntryModel = Backbone.Model.extend({
|
|||
this.entry.times.update();
|
||||
},
|
||||
|
||||
setSaved: function() {
|
||||
if (this.unsaved) {
|
||||
this.unsaved = false;
|
||||
}
|
||||
if (this.canBeDeleted) {
|
||||
this.canBeDeleted = false;
|
||||
}
|
||||
},
|
||||
|
||||
matches: function(filter) {
|
||||
return !filter ||
|
||||
(!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
|
||||
|
@ -267,6 +292,19 @@ var EntryModel = Backbone.Model.extend({
|
|||
this._fillByEntry();
|
||||
},
|
||||
|
||||
renameTag: function(from, to) {
|
||||
var ix = _.findIndex(this.entry.tags, function(tag) { return tag.toLowerCase() === from.toLowerCase(); });
|
||||
if (ix < 0) {
|
||||
return;
|
||||
}
|
||||
this._entryModified();
|
||||
this.entry.tags.splice(ix, 1);
|
||||
if (to) {
|
||||
this.entry.tags.push(to);
|
||||
}
|
||||
this._fillByEntry();
|
||||
},
|
||||
|
||||
setField: function(field, val) {
|
||||
var hasValue = val && (typeof val === 'string' || val.isProtected && val.byteLength);
|
||||
if (hasValue || this.builtInFields.indexOf(field) >= 0) {
|
||||
|
@ -363,11 +401,28 @@ var EntryModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
removeWithoutHistory: function() {
|
||||
var ix = this.group.group.entries.indexOf(this.entry);
|
||||
if (ix >= 0) {
|
||||
this.group.group.entries.splice(ix, 1);
|
||||
if (this.canBeDeleted) {
|
||||
var ix = this.group.group.entries.indexOf(this.entry);
|
||||
if (ix >= 0) {
|
||||
this.group.group.entries.splice(ix, 1);
|
||||
}
|
||||
this.file.reload();
|
||||
}
|
||||
},
|
||||
|
||||
moveToFile: function(file) {
|
||||
if (this.canBeDeleted) {
|
||||
this.removeWithoutHistory();
|
||||
this.group = file.get('groups').first();
|
||||
this.file = file;
|
||||
this._fillByEntry();
|
||||
this.entry.times.update();
|
||||
this.group.group.entries.push(this.entry);
|
||||
this.group.addEntry(this);
|
||||
this.isJustCreated = true;
|
||||
this.unsaved = true;
|
||||
this.file.setModified();
|
||||
}
|
||||
this.file.reload();
|
||||
},
|
||||
|
||||
initOtpGenerator: function() {
|
||||
|
@ -438,6 +493,49 @@ var EntryModel = Backbone.Model.extend({
|
|||
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
|
||||
delete this.entry.fields['TOTP Seed'];
|
||||
delete this.entry.fields['TOTP Settings'];
|
||||
},
|
||||
|
||||
getEffectiveEnableAutoType: function() {
|
||||
if (typeof this.entry.autoType.enabled === 'boolean') {
|
||||
return this.entry.autoType.enabled;
|
||||
}
|
||||
return this.group.getEffectiveEnableAutoType();
|
||||
},
|
||||
|
||||
getEffectiveAutoTypeSeq: function() {
|
||||
return this.entry.autoType.defaultSequence || this.group.getEffectiveAutoTypeSeq();
|
||||
},
|
||||
|
||||
setEnableAutoType: function(enabled) {
|
||||
this._entryModified();
|
||||
if (enabled === this.group.getEffectiveEnableAutoType()) {
|
||||
enabled = null;
|
||||
}
|
||||
this.entry.autoType.enabled = enabled;
|
||||
this._buildAutoType();
|
||||
},
|
||||
|
||||
setAutoTypeObfuscation: function(enabled) {
|
||||
this._entryModified();
|
||||
this.entry.autoType.obfuscation =
|
||||
enabled ? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard : kdbxweb.Consts.AutoTypeObfuscationOptions.None;
|
||||
this._buildAutoType();
|
||||
},
|
||||
|
||||
setAutoTypeSeq: function(seq) {
|
||||
this._entryModified();
|
||||
this.entry.autoType.defaultSequence = seq || undefined;
|
||||
this._buildAutoType();
|
||||
},
|
||||
|
||||
getGroupPath: function() {
|
||||
var group = this.group;
|
||||
var groupPath = [];
|
||||
while (group) {
|
||||
groupPath.unshift(group.get('title'));
|
||||
group = group.parentGroup;
|
||||
}
|
||||
return groupPath;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -454,6 +552,7 @@ EntryModel.newEntry = function(group, file) {
|
|||
model.entry.times.update();
|
||||
model.unsaved = true;
|
||||
model.isJustCreated = true;
|
||||
model.canBeDeleted = true;
|
||||
group.addEntry(model);
|
||||
file.setModified();
|
||||
return model;
|
||||
|
|
|
@ -13,6 +13,7 @@ var logger = new Logger('file');
|
|||
var FileModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
id: '',
|
||||
uuid: '',
|
||||
name: '',
|
||||
keyFileName: '',
|
||||
passwordLength: 0,
|
||||
|
@ -29,10 +30,10 @@ var FileModel = Backbone.Model.extend({
|
|||
oldKeyFileName: '',
|
||||
passwordChanged: false,
|
||||
keyFileChanged: false,
|
||||
keyChangeForce: -1,
|
||||
syncing: false,
|
||||
syncError: null,
|
||||
syncDate: null,
|
||||
cacheId: null
|
||||
syncDate: null
|
||||
},
|
||||
|
||||
db: null,
|
||||
|
@ -136,27 +137,31 @@ var FileModel = Backbone.Model.extend({
|
|||
readModel: function() {
|
||||
var groups = new GroupCollection();
|
||||
this.set({
|
||||
id: this.db.getDefaultGroup().uuid.toString(),
|
||||
uuid: this.db.getDefaultGroup().uuid.toString(),
|
||||
groups: groups,
|
||||
defaultUser: this.db.meta.defaultUser,
|
||||
recycleBinEnabled: this.db.meta.recycleBinEnabled,
|
||||
historyMaxItems: this.db.meta.historyMaxItems,
|
||||
historyMaxSize: this.db.meta.historyMaxSize,
|
||||
keyEncryptionRounds: this.db.header.keyEncryptionRounds
|
||||
keyEncryptionRounds: this.db.header.keyEncryptionRounds,
|
||||
keyChangeForce: this.db.meta.keyChangeForce
|
||||
}, { silent: true });
|
||||
this.db.groups.forEach(function(group) {
|
||||
var groupModel = this.getGroup(group.uuid.id);
|
||||
var groupModel = this.getGroup(this.subId(group.uuid.id));
|
||||
if (groupModel) {
|
||||
groupModel.setGroup(group, this);
|
||||
} else {
|
||||
groupModel = GroupModel.fromGroup(group, this);
|
||||
}
|
||||
groupModel.set({title: this.get('name')});
|
||||
groups.add(groupModel);
|
||||
}, this);
|
||||
this.buildObjectMap();
|
||||
},
|
||||
|
||||
subId: function(id) {
|
||||
return this.id + ':' + id;
|
||||
},
|
||||
|
||||
buildObjectMap: function() {
|
||||
var entryMap = {};
|
||||
var groupMap = {};
|
||||
|
@ -257,7 +262,7 @@ var FileModel = Backbone.Model.extend({
|
|||
forEachEntry: function(filter, callback) {
|
||||
var top = this;
|
||||
if (filter.trash) {
|
||||
top = this.getGroup(this.db.meta.recycleBinUuid ? this.db.meta.recycleBinUuid.id : null);
|
||||
top = this.getGroup(this.db.meta.recycleBinUuid ? this.subId(this.db.meta.recycleBinUuid.id) : null);
|
||||
} else if (filter.group) {
|
||||
top = this.getGroup(filter.group);
|
||||
}
|
||||
|
@ -282,7 +287,7 @@ var FileModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
getTrashGroup: function() {
|
||||
return this.db.meta.recycleBinEnabled ? this.getGroup(this.db.meta.recycleBinUuid.id) : null;
|
||||
return this.db.meta.recycleBinEnabled ? this.getGroup(this.subId(this.db.meta.recycleBinUuid.id)) : null;
|
||||
},
|
||||
|
||||
setModified: function() {
|
||||
|
@ -340,7 +345,7 @@ var FileModel = Backbone.Model.extend({
|
|||
}
|
||||
this.setOpenFile({ passwordLength: this.get('passwordLength') });
|
||||
this.forEachEntry({}, function(entry) {
|
||||
entry.unsaved = false;
|
||||
entry.setSaved();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -391,6 +396,28 @@ var FileModel = Backbone.Model.extend({
|
|||
this.setModified();
|
||||
},
|
||||
|
||||
isKeyChangePending: function(force) {
|
||||
if (!this.db.meta.keyChanged) {
|
||||
return false;
|
||||
}
|
||||
var expiryDays = force ? this.db.meta.keyChangeForce : this.db.meta.keyChangeRec;
|
||||
if (!expiryDays || expiryDays < 0 || isNaN(expiryDays)) {
|
||||
return false;
|
||||
}
|
||||
var daysDiff = (Date.now() - this.db.meta.keyChanged) / 1000 / 3600 / 24;
|
||||
return daysDiff > expiryDays;
|
||||
},
|
||||
|
||||
setKeyChange: function(force, days) {
|
||||
if (isNaN(days) || !days || days < 0) {
|
||||
days = -1;
|
||||
}
|
||||
var prop = force ? 'keyChangeForce' : 'keyChangeRec';
|
||||
this.db.meta[prop] = days;
|
||||
this.set(prop, days);
|
||||
this.setModified();
|
||||
},
|
||||
|
||||
setName: function(name) {
|
||||
this.db.meta.name = name;
|
||||
this.db.meta.nameChanged = new Date();
|
||||
|
@ -455,9 +482,15 @@ var FileModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
addCustomIcon: function(iconData) {
|
||||
var id = kdbxweb.KdbxUuid.random();
|
||||
this.db.meta.customIcons[id] = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData));
|
||||
return id.toString();
|
||||
var uuid = kdbxweb.KdbxUuid.random();
|
||||
this.db.meta.customIcons[uuid] = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData));
|
||||
return uuid.toString();
|
||||
},
|
||||
|
||||
renameTag: function(from, to) {
|
||||
this.forEachEntry({}, function(entry) {
|
||||
entry.renameTag(from, to);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ var MenuItemModel = require('./menu/menu-item-model'),
|
|||
KdbxIcons = kdbxweb.Consts.Icons,
|
||||
GroupCollection, EntryCollection;
|
||||
|
||||
var DefaultAutoTypeSequence = '{USERNAME}{TAB}{PASSWORD}{ENTER}';
|
||||
|
||||
var GroupModel = MenuItemModel.extend({
|
||||
defaults: _.extend({}, MenuItemModel.prototype.defaults, {
|
||||
iconId: 0,
|
||||
|
@ -17,7 +19,9 @@ var GroupModel = MenuItemModel.extend({
|
|||
top: false,
|
||||
drag: true,
|
||||
drop: true,
|
||||
enableSearching: true
|
||||
enableSearching: true,
|
||||
enableAutoType: null,
|
||||
autoTypeSeq: null
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
|
@ -27,16 +31,21 @@ var GroupModel = MenuItemModel.extend({
|
|||
|
||||
setGroup: function(group, file, parentGroup) {
|
||||
var isRecycleBin = file.db.meta.recycleBinUuid && file.db.meta.recycleBinUuid.id === group.uuid.id;
|
||||
var id = file.subId(group.uuid.id);
|
||||
this.set({
|
||||
id: group.uuid.id,
|
||||
id: id,
|
||||
uuid: group.uuid.id,
|
||||
expanded: group.expanded,
|
||||
visible: !isRecycleBin,
|
||||
items: new GroupCollection(),
|
||||
entries: new EntryCollection(),
|
||||
filterValue: group.uuid.id,
|
||||
filterValue: id,
|
||||
enableSearching: group.enableSearching,
|
||||
enableAutoType: group.enableAutoType,
|
||||
autoTypeSeq: group.defaultAutoTypeSeq,
|
||||
top: !parentGroup,
|
||||
drag: !!parentGroup
|
||||
drag: !!parentGroup,
|
||||
collapsible: !!parentGroup
|
||||
}, { silent: true });
|
||||
this.group = group;
|
||||
this.file = file;
|
||||
|
@ -45,7 +54,7 @@ var GroupModel = MenuItemModel.extend({
|
|||
var items = this.get('items'),
|
||||
entries = this.get('entries');
|
||||
group.groups.forEach(function(subGroup) {
|
||||
var existing = file.getGroup(subGroup.uuid);
|
||||
var existing = file.getGroup(file.subId(subGroup.uuid.id));
|
||||
if (existing) {
|
||||
existing.setGroup(subGroup, file, this);
|
||||
items.add(existing);
|
||||
|
@ -54,7 +63,7 @@ var GroupModel = MenuItemModel.extend({
|
|||
}
|
||||
}, this);
|
||||
group.entries.forEach(function(entry) {
|
||||
var existing = file.getEntry(entry.uuid);
|
||||
var existing = file.getEntry(file.subId(entry.uuid.id));
|
||||
if (existing) {
|
||||
existing.setEntry(entry, this, file);
|
||||
entries.add(existing);
|
||||
|
@ -66,11 +75,12 @@ var GroupModel = MenuItemModel.extend({
|
|||
|
||||
_fillByGroup: function(silent) {
|
||||
this.set({
|
||||
title: this.group.name,
|
||||
title: this.parentGroup ? this.group.name : this.file.get('name'),
|
||||
iconId: this.group.icon,
|
||||
icon: this._iconFromId(this.group.icon),
|
||||
customIcon: this._buildCustomIcon(),
|
||||
customIconId: this.group.customIcon ? this.group.customIcon.toString() : null
|
||||
customIconId: this.group.customIcon ? this.group.customIcon.toString() : null,
|
||||
expanded: this.group.expanded !== false
|
||||
}, { silent: silent });
|
||||
},
|
||||
|
||||
|
@ -93,7 +103,8 @@ var GroupModel = MenuItemModel.extend({
|
|||
if (this.isJustCreated) {
|
||||
this.isJustCreated = false;
|
||||
}
|
||||
// this.group.times.update(); // for now, we don't remember this setting
|
||||
this.file.setModified();
|
||||
this.group.times.update();
|
||||
},
|
||||
|
||||
forEachGroup: function(callback, includeDisabled) {
|
||||
|
@ -146,15 +157,88 @@ var GroupModel = MenuItemModel.extend({
|
|||
},
|
||||
|
||||
setExpanded: function(expanded) {
|
||||
this._groupModified();
|
||||
// this._groupModified(); // it's not good to mark the file as modified when a group is collapsed
|
||||
this.group.expanded = expanded;
|
||||
this.set('expanded', expanded);
|
||||
},
|
||||
|
||||
setEnableSearching: function(enabled) {
|
||||
this._groupModified();
|
||||
var parentEnableSearching = true;
|
||||
var parentGroup = this.parentGroup;
|
||||
while (parentGroup) {
|
||||
if (typeof parentGroup.get('enableSearching') === 'boolean') {
|
||||
parentEnableSearching = parentGroup.get('enableSearching');
|
||||
break;
|
||||
}
|
||||
parentGroup = parentGroup.parentGroup;
|
||||
}
|
||||
if (enabled === parentEnableSearching) {
|
||||
enabled = null;
|
||||
}
|
||||
this.group.enableSearching = enabled;
|
||||
this.set('enableSearching', enabled);
|
||||
this.set('enableSearching', this.group.enableSearching);
|
||||
},
|
||||
|
||||
getEffectiveEnableSearching: function() {
|
||||
var grp = this;
|
||||
while (grp) {
|
||||
if (typeof grp.get('enableSearching') === 'boolean') {
|
||||
return grp.get('enableSearching');
|
||||
}
|
||||
grp = grp.parentGroup;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
setEnableAutoType: function(enabled) {
|
||||
this._groupModified();
|
||||
var parentEnableAutoType = true;
|
||||
var parentGroup = this.parentGroup;
|
||||
while (parentGroup) {
|
||||
if (typeof parentGroup.get('enableAutoType') === 'boolean') {
|
||||
parentEnableAutoType = parentGroup.get('enableAutoType');
|
||||
break;
|
||||
}
|
||||
parentGroup = parentGroup.parentGroup;
|
||||
}
|
||||
if (enabled === parentEnableAutoType) {
|
||||
enabled = null;
|
||||
}
|
||||
this.group.enableAutoType = enabled;
|
||||
this.set('enableAutoType', this.group.enableAutoType);
|
||||
},
|
||||
|
||||
getEffectiveEnableAutoType: function() {
|
||||
var grp = this;
|
||||
while (grp) {
|
||||
if (typeof grp.get('enableAutoType') === 'boolean') {
|
||||
return grp.get('enableAutoType');
|
||||
}
|
||||
grp = grp.parentGroup;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
setAutoTypeSeq: function(seq) {
|
||||
this._groupModified();
|
||||
this.group.defaultAutoTypeSeq = seq || undefined;
|
||||
this.set('autoTypeSeq', this.group.defaultAutoTypeSeq);
|
||||
},
|
||||
|
||||
getEffectiveAutoTypeSeq: function() {
|
||||
var grp = this;
|
||||
while (grp) {
|
||||
if (grp.get('autoTypeSeq')) {
|
||||
return grp.get('autoTypeSeq');
|
||||
}
|
||||
grp = grp.parentGroup;
|
||||
}
|
||||
return DefaultAutoTypeSequence;
|
||||
},
|
||||
|
||||
getParentEffectiveAutoTypeSeq: function() {
|
||||
return this.parentGroup ? this.parentGroup.getEffectiveAutoTypeSeq() : DefaultAutoTypeSequence;
|
||||
},
|
||||
|
||||
moveToTrash: function() {
|
||||
|
|
|
@ -20,7 +20,8 @@ var MenuItemModel = Backbone.Model.extend({
|
|||
drag: false,
|
||||
drop: false,
|
||||
filterKey: null,
|
||||
filterValue: null
|
||||
filterValue: null,
|
||||
collapsible: false
|
||||
},
|
||||
|
||||
initialize: function(model) {
|
||||
|
|
|
@ -27,20 +27,27 @@ var MenuItemModel = Backbone.Model.extend({
|
|||
this.trigger('change-items');
|
||||
},
|
||||
|
||||
removeByFile: function(file, skipEvent) {
|
||||
removeByFile: function(file) {
|
||||
var items = this.get('items');
|
||||
var toRemove;
|
||||
items.each(function(item) {
|
||||
items.find(function(item) {
|
||||
if (item.file === file || item.get('file') === file) {
|
||||
toRemove = item;
|
||||
items.remove(item);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (toRemove) {
|
||||
items.remove(toRemove);
|
||||
}
|
||||
if (!skipEvent) {
|
||||
this.trigger('change-items');
|
||||
}
|
||||
this.trigger('change-items');
|
||||
},
|
||||
|
||||
replaceByFile: function(file, newItem) {
|
||||
var items = this.get('items');
|
||||
items.find(function(item, ix) {
|
||||
if (item.file === file || item.get('file') === file) {
|
||||
items.remove(item);
|
||||
items.add(newItem, { at: ix });
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.trigger('change-items');
|
||||
},
|
||||
|
||||
setItems: function(items) {
|
||||
|
|
|
@ -19,7 +19,7 @@ EntryPresenter.prototype = {
|
|||
}
|
||||
return this;
|
||||
},
|
||||
get id() { return this.entry ? this.entry.id : this.group.get('id'); },
|
||||
get id() { return this.entry ? this.entry.id : this.group.id; },
|
||||
get icon() { return this.entry ? this.entry.icon : (this.group.get('icon') || 'folder'); },
|
||||
get customIcon() { return this.entry ? this.entry.customIcon : undefined; },
|
||||
get color() { return this.entry ? (this.entry.color || (this.entry.customIcon ? this.noColor : undefined)) : undefined; },
|
||||
|
@ -31,7 +31,9 @@ EntryPresenter.prototype = {
|
|||
get created() { return this.entry ? Format.dtStr(this.entry.created) : undefined; },
|
||||
get updated() { return this.entry ? Format.dtStr(this.entry.updated) : undefined; },
|
||||
get expired() { return this.entry ? this.entry.expired : false; },
|
||||
get tags() { return this.entry ? this.entry.tags : false; },
|
||||
get tags() { return this.entry ? this.entry.tags : undefined; },
|
||||
get groupName() { return this.entry ? this.entry.groupName : undefined; },
|
||||
get fileName() { return this.entry ? this.entry.fileName : undefined; },
|
||||
get description() {
|
||||
if (!this.entry) {
|
||||
return '[' + Locale.listGroup + ']';
|
||||
|
|
|
@ -37,6 +37,10 @@ _.extend(StorageBase.prototype, {
|
|||
return this;
|
||||
},
|
||||
|
||||
setEnabled: function(enabled) {
|
||||
this.enabled = enabled;
|
||||
},
|
||||
|
||||
_xhr: function(config) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
if (config.responseType) {
|
||||
|
@ -186,6 +190,18 @@ _.extend(StorageBase.prototype, {
|
|||
this._oauthToken.expired = true;
|
||||
this.runtimeData.set(this.name + 'OAuthToken', this._oauthToken);
|
||||
this._oauthAuthorize(callback);
|
||||
},
|
||||
|
||||
_oauthRevokeToken: function(url) {
|
||||
var token = this.runtimeData.get(this.name + 'OAuthToken');
|
||||
if (token) {
|
||||
this._xhr({
|
||||
url: url.replace('{token}', token.accessToken),
|
||||
statuses: [200, 401]
|
||||
});
|
||||
this.runtimeData.unset(this.name + 'OAuthToken');
|
||||
this._oauthToken = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -223,6 +223,13 @@ var StorageDropbox = StorageBase.extend({
|
|||
that.logger.debug('Removed', path, that.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}, _.noop);
|
||||
},
|
||||
|
||||
setEnabled: function(enabled) {
|
||||
if (!enabled) {
|
||||
DropboxLink.logout();
|
||||
}
|
||||
StorageBase.prototype.setEnabled.call(this, enabled);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -180,6 +180,13 @@ var StorageGDrive = StorageBase.extend({
|
|||
});
|
||||
},
|
||||
|
||||
setEnabled: function(enabled) {
|
||||
if (!enabled) {
|
||||
this._oauthRevokeToken('https://accounts.google.com/o/oauth2/revoke?token={token}');
|
||||
}
|
||||
StorageBase.prototype.setEnabled.call(this, enabled);
|
||||
},
|
||||
|
||||
_getOAuthConfig: function() {
|
||||
var clientId = this.appSettings.get('gdriveClientId') || GDriveClientId;
|
||||
return {
|
||||
|
|
|
@ -197,6 +197,16 @@ var StorageOneDrive = StorageBase.extend({
|
|||
});
|
||||
},
|
||||
|
||||
setEnabled: function(enabled) {
|
||||
if (!enabled) {
|
||||
var url = 'https://login.live.com/oauth20_logout.srf?client_id={client_id}&redirect_uri={url}'
|
||||
.replace('{client_id}', this._getClientId())
|
||||
.replace('{url}', this._getOauthRedirectUrl());
|
||||
this._oauthRevokeToken(url);
|
||||
}
|
||||
StorageBase.prototype.setEnabled.call(this, enabled);
|
||||
},
|
||||
|
||||
_getClientId: function() {
|
||||
var clientId = this.appSettings.get('onedriveClientId');
|
||||
if (!clientId) {
|
||||
|
|
|
@ -84,9 +84,13 @@ var StorageWebDav = StorageBase.extend({
|
|||
that._request(_.defaults({ op: 'Save:delete', method: 'DELETE', path: tmpPath }, saveOpts));
|
||||
return cb({ revConflict: true }, xhr, stat);
|
||||
}
|
||||
var movePath = path;
|
||||
if (movePath.indexOf('://') < 0) {
|
||||
movePath = location.href.replace(/[^/]*$/, movePath);
|
||||
}
|
||||
that._request(_.defaults({
|
||||
op: 'Save:move', method: 'MOVE', path: tmpPath, nostat: true,
|
||||
headers: { Destination: path, 'Overwrite': 'T' }
|
||||
headers: { Destination: movePath, 'Overwrite': 'T' }
|
||||
}, saveOpts), function(err) {
|
||||
if (err) { return cb(err); }
|
||||
that._request(_.defaults({
|
||||
|
@ -103,7 +107,7 @@ var StorageWebDav = StorageBase.extend({
|
|||
fileOptsToStoreOpts: function(opts, file) {
|
||||
var result = {user: opts.user, encpass: opts.encpass};
|
||||
if (opts.password) {
|
||||
var fileId = file.get('id');
|
||||
var fileId = file.get('uuid');
|
||||
var password = opts.password;
|
||||
var encpass = '';
|
||||
for (var i = 0; i < password.length; i++) {
|
||||
|
@ -117,7 +121,7 @@ var StorageWebDav = StorageBase.extend({
|
|||
storeOptsToFileOpts: function(opts, file) {
|
||||
var result = {user: opts.user, password: opts.password};
|
||||
if (opts.encpass) {
|
||||
var fileId = file.get('id');
|
||||
var fileId = file.get('uuid');
|
||||
var encpass = atob(opts.encpass);
|
||||
var password = '';
|
||||
for (var i = 0; i < encpass.length; i++) {
|
||||
|
@ -173,8 +177,8 @@ var StorageWebDav = StorageBase.extend({
|
|||
that.logger.debug(config.op + ' error', config.path, 'aborted', that.logger.ts(ts));
|
||||
if (callback) { callback('aborted', xhr); callback = null; }
|
||||
});
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.open(config.method, config.path);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
if (config.user) {
|
||||
xhr.setRequestHeader('Authorization', 'Basic ' + btoa(config.user + ':' + config.password));
|
||||
}
|
||||
|
|
|
@ -10,12 +10,21 @@ var Format = {
|
|||
}
|
||||
return str;
|
||||
},
|
||||
padStr: function(str, len) {
|
||||
while (str.length < len) {
|
||||
str += ' ';
|
||||
}
|
||||
return str;
|
||||
},
|
||||
dtStr: function(dt) {
|
||||
return dt ? this.dStr(dt) + ' ' + this.pad(dt.getHours(), 2) + ':' + this.pad(dt.getMinutes(), 2) +
|
||||
':' + this.pad(dt.getSeconds(), 2) : '';
|
||||
},
|
||||
dStr: function(dt) {
|
||||
return dt ? dt.getDate() + ' ' + Locale.monthsShort[dt.getMonth()] + ' ' + dt.getFullYear() : '';
|
||||
},
|
||||
capFirst: function(str) {
|
||||
return str[0].toUpperCase() + str.substr(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ var Locale = {
|
|||
website: 'website',
|
||||
tags: 'tags',
|
||||
notes: 'notes',
|
||||
group: 'group',
|
||||
noTitle: 'no title',
|
||||
or: 'or',
|
||||
notImplemented: 'Not Implemented',
|
||||
|
@ -39,6 +40,7 @@ var Locale = {
|
|||
menuEmptyTrash: 'Empty Trash',
|
||||
menuEmptyTrashAlert: 'Empty Trash?',
|
||||
menuEmptyTrashAlertBody: 'You will not be able to put items back',
|
||||
menuItemCollapsed: 'Double-click to expand',
|
||||
|
||||
alertYes: 'Yes',
|
||||
alertNo: 'No',
|
||||
|
@ -71,9 +73,25 @@ var Locale = {
|
|||
|
||||
grpTitle: 'Group',
|
||||
grpSearch: 'Enable searching entries in this group',
|
||||
grpAutoType: 'Enable auto-type',
|
||||
grpAutoTypeSeq: 'Auto-type sequence',
|
||||
grpAutoTypeSeqDefault: 'Use default auto-type sequence',
|
||||
grpTrash: 'Delete group with all entries',
|
||||
|
||||
keyChangeTitle: 'Master Key Changed',
|
||||
keyChangeMessage: 'Master key was changed for this database. Please enter a new key',
|
||||
tagTitle: 'Tag',
|
||||
tagTrash: 'Remove tag from all entries',
|
||||
tagRename: 'Rename',
|
||||
tagTrashQuestion: 'Remove tag from all entries?',
|
||||
tagTrashQuestionBody: 'This tag will be removed from all entries. There will be no easy way to put it back.',
|
||||
tagExists: 'Tag already exists',
|
||||
tagExistsBody: 'Tag with this name already exists. Please choose another name.',
|
||||
tagBadName: 'Bad name',
|
||||
tagBadNameBody: 'Tag name can not contain characters `,`, `;`, `:`. Please remove them.',
|
||||
|
||||
keyChangeTitleRemote: 'Master Key Changed',
|
||||
keyChangeMessageRemote: 'Master key was changed for this database. Please enter a new key',
|
||||
keyChangeTitleExpired: 'Master Key Expired',
|
||||
keyChangeMessageExpired: 'Master key for this database is expired. Please enter a new key',
|
||||
|
||||
iconFavTitle: 'Download and use website favicon',
|
||||
iconSelCustom: 'Select custom icon',
|
||||
|
@ -113,6 +131,7 @@ var Locale = {
|
|||
openDemo: 'Demo',
|
||||
openSettings: 'Settings',
|
||||
openCaps: 'Caps Lock is on',
|
||||
openClickToOpen: 'Click to open a file',
|
||||
openKeyFile: 'key file',
|
||||
openKeyFileDropbox: '(from dropbox)',
|
||||
openDropHere: 'drop files here',
|
||||
|
@ -171,7 +190,7 @@ var Locale = {
|
|||
detHistoryCurState: 'current state',
|
||||
detHistoryCurUnsavedState: 'current unsaved state',
|
||||
detBackToList: 'back to list',
|
||||
detSetIconColor: 'Change icon color',
|
||||
detSetIconColor: 'Change color',
|
||||
detSetIcon: 'Change icon',
|
||||
detDropAttachments: 'drop attachments here',
|
||||
detDelEntry: 'Delete',
|
||||
|
@ -184,6 +203,7 @@ var Locale = {
|
|||
detExpires: 'Expires',
|
||||
detExpired: 'expired',
|
||||
detFile: 'File',
|
||||
detGroup: 'Group',
|
||||
detCreated: 'Created',
|
||||
detUpdated: 'Updated',
|
||||
detHistory: 'History',
|
||||
|
@ -202,13 +222,22 @@ var Locale = {
|
|||
detMenuHideEmpty: 'Hide empty fields',
|
||||
detMenuAddField: 'Add {}',
|
||||
detSetupOtp: 'One-time passwords',
|
||||
detAutoType: 'Auto-type',
|
||||
detAutoTypeEnabled: 'Enable auto-type for this entry',
|
||||
detAutoTypeSequence: 'Keystrokes',
|
||||
detAutoTypeInput: 'Input',
|
||||
detAutoTypeShortcuts: 'Shortcuts',
|
||||
detAutoTypeShortcutsDesc: '{} or {} while the app is inactive',
|
||||
detAutoTypeObfuscation: 'Mix real keystrokes with random',
|
||||
detAutoTypeWindow: 'Window',
|
||||
detAutoTypeInputWindow: 'Window title',
|
||||
detSetupOtpAlert: 'Scan the QR code',
|
||||
detSetupOtpAlertBody: 'Please copy the QR code which is displayed on the authorization page.',
|
||||
detSetupOtpAlertBody1: '1. go to the authorization page',
|
||||
detSetupOtpAlertBody2: '2. make a screenshot of the QR code {}',
|
||||
detSetupOtpAlertBody3: '3. paste it here {}',
|
||||
detSetupOtpAlertBody3Mobile: '3. select it or scan with your camera using Select/Scan button below',
|
||||
detSetupOtpAlertBody4: 'If you can\'t scan code, click Enter code manually',
|
||||
detSetupOtpAlertBody4: 'If you can\'t scan the code, click Enter code manually',
|
||||
detSetupOtpManualButton: 'Enter code manually',
|
||||
detSetupOtpScanButton: 'Select/Scan',
|
||||
detSetupOtpAlertBodyWith: 'with {}',
|
||||
|
@ -220,6 +249,11 @@ var Locale = {
|
|||
detOtpQrWrong: 'Wrong QR code',
|
||||
detOtpQrWrongBody: 'Your QR code was successfully scanned but it doesn\'t contain one-time password data.',
|
||||
|
||||
autoTypeEntryFields: 'Entry fields',
|
||||
autoTypeModifiers: 'Modifier keys',
|
||||
autoTypeKeys: 'Keys',
|
||||
autoTypeLink: 'more...',
|
||||
|
||||
appSecWarn: 'Not Secure!',
|
||||
appSecWarnBody1: 'You have loaded this app with insecure connection. ' +
|
||||
'Someone may be watching you and stealing your passwords. ' +
|
||||
|
@ -239,6 +273,8 @@ var Locale = {
|
|||
appSaveError: 'Save Error',
|
||||
appSaveErrorBody: 'Failed to auto-save file',
|
||||
appSaveErrorBodyMul: 'Failed to auto-save files:',
|
||||
appSettingsError: 'Error loading app',
|
||||
appSettingsErrorBody: 'There was an error loading app settings. Please double check app url or contact your administrator.',
|
||||
|
||||
setGenTitle: 'General Settings',
|
||||
setGenUpdate: 'Update',
|
||||
|
@ -267,6 +303,14 @@ var Locale = {
|
|||
setGenDownloadAndRestart: 'Download update and restart',
|
||||
setGenAppearance: 'Appearance',
|
||||
setGenTheme: 'Theme',
|
||||
setGenThemeFb: 'Flat blue',
|
||||
setGenThemeDb: 'Dark brown',
|
||||
setGenThemeWh: 'White',
|
||||
setGenThemeHc: 'High contrast',
|
||||
setGenFontSize: 'Font size',
|
||||
setGenFontSizeNormal: 'Normal',
|
||||
setGenFontSizeLarge: 'Large',
|
||||
setGenFontSizeLargest: 'Largest',
|
||||
setGenShowSubgroups: 'Show entries from all subgroups',
|
||||
setGenTableView: 'Entries list table view',
|
||||
setGenColorfulIcons: 'Colorful custom icons in list',
|
||||
|
@ -286,14 +330,16 @@ var Locale = {
|
|||
setGenLockCopy: 'Auto-lock on password copy',
|
||||
setGenStorage: 'Storage',
|
||||
setGenAdvanced: 'Advanced',
|
||||
setGenShowAdvanced: 'Show advanced settings (these settings may be dangerous)',
|
||||
setGenDevTools: 'Show dev tools',
|
||||
setGenTryBeta: 'Try beta version for one time',
|
||||
setGenTryBetaWarning: 'Unsaved files',
|
||||
setGenTryBetaWarningBody: 'Please save all files and click this button again',
|
||||
setGenShowAppLogs: 'Show app logs',
|
||||
|
||||
setFilePath: 'File path',
|
||||
setFileStorage: 'This file is opened from {}.',
|
||||
setFileIntl: 'This file is stored in internal app storage',
|
||||
setFileIntl: 'This file is stored in the internal app storage',
|
||||
setFileLocalHint: 'Want to work seamlessly with local files?',
|
||||
setFileDownloadApp: 'Download a desktop app',
|
||||
setFileSave: 'Save',
|
||||
|
@ -320,6 +366,7 @@ var Locale = {
|
|||
setFileHistSize: 'History size, total MB per file',
|
||||
setFileAdvanced: 'Advanced',
|
||||
setFileRounds: 'Key encryption rounds',
|
||||
setFileKeyChangeForce: 'Ask to change key after (days)',
|
||||
setFileUseKeyFile: 'Use key file',
|
||||
setFileUseGenKeyFile: 'Use generated key file',
|
||||
setFileUseOldKeyFile: 'Use old key file',
|
||||
|
@ -348,6 +395,7 @@ var Locale = {
|
|||
setShCopyPass: 'copy password or selected field',
|
||||
setShCopyUser: 'copy username',
|
||||
setShCopyUrl: 'copy website',
|
||||
setShAutoType: 'auto-type selected entry',
|
||||
setShPrev: 'go to previous item',
|
||||
setShNext: 'go to next item',
|
||||
setShCreateEntry: 'create entry',
|
||||
|
@ -358,6 +406,7 @@ var Locale = {
|
|||
setShCopyPassGlobal: 'copy password (when app is in background)',
|
||||
setShCopyUserGlobal: 'copy username (when app is in background)',
|
||||
setShCopyUrlGlobal: 'copy website (when app is in background)',
|
||||
setShAutoTypeGlobal: 'auto-type (when app is in background)',
|
||||
setShLock: 'lock database',
|
||||
|
||||
setAboutTitle: 'About',
|
||||
|
|
|
@ -3,8 +3,22 @@
|
|||
/* globals console */
|
||||
/* globals performance */
|
||||
|
||||
var Level = {
|
||||
Off: 0,
|
||||
Error: 1,
|
||||
Warn: 2,
|
||||
Info: 3,
|
||||
Debug: 4,
|
||||
All: 5
|
||||
};
|
||||
|
||||
var MaxLogsToSave = 100;
|
||||
|
||||
var lastLogs = [];
|
||||
|
||||
var Logger = function(name, id) {
|
||||
this.prefix = (name ? name + (id ? ':' + id : '') : 'default');
|
||||
this.level = Level.All;
|
||||
};
|
||||
|
||||
Logger.prototype.ts = function(ts) {
|
||||
|
@ -21,22 +35,55 @@ Logger.prototype.getPrefix = function() {
|
|||
|
||||
Logger.prototype.debug = function() {
|
||||
arguments[0] = this.getPrefix() + arguments[0];
|
||||
console.debug.apply(console, arguments);
|
||||
if (this.level > Level.Debug) {
|
||||
Logger.saveLast('debug', arguments);
|
||||
console.debug.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Logger.prototype.info = function() {
|
||||
arguments[0] = this.getPrefix() + arguments[0];
|
||||
console.log.apply(console, arguments);
|
||||
if (this.level > Level.Info) {
|
||||
Logger.saveLast('info', arguments);
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Logger.prototype.warn = function() {
|
||||
arguments[0] = this.getPrefix() + arguments[0];
|
||||
console.warn.apply(console, arguments);
|
||||
if (this.level > Level.Warn) {
|
||||
Logger.saveLast('warn', arguments);
|
||||
console.warn.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Logger.prototype.error = function() {
|
||||
arguments[0] = this.getPrefix() + arguments[0];
|
||||
console.error.apply(console, arguments);
|
||||
if (this.level > Level.Error) {
|
||||
Logger.saveLast('error', arguments);
|
||||
console.error.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Logger.prototype.setLevel = function(level) {
|
||||
this.level = level;
|
||||
};
|
||||
|
||||
Logger.prototype.getLevel = function() {
|
||||
return this.level;
|
||||
};
|
||||
|
||||
Logger.saveLast = function(level, args) {
|
||||
lastLogs.push({ level: level, args: Array.prototype.slice.call(args) });
|
||||
if (lastLogs.length > MaxLogsToSave) {
|
||||
lastLogs.shift();
|
||||
}
|
||||
};
|
||||
|
||||
Logger.getLast = function() {
|
||||
return lastLogs;
|
||||
};
|
||||
|
||||
Logger.Level = Level;
|
||||
|
||||
module.exports = Logger;
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
var ThemeChanger = {
|
||||
setBySettings: function(settings) {
|
||||
if (settings.get('theme')) {
|
||||
this.setTheme(settings.get('theme'));
|
||||
}
|
||||
if (settings.get('fontSize')) {
|
||||
this.setFontSize(settings.get('fontSize'));
|
||||
}
|
||||
},
|
||||
|
||||
setTheme: function(theme) {
|
||||
_.forEach(document.body.classList, function(cls) {
|
||||
if (/^th\-/.test(cls)) {
|
||||
|
@ -12,6 +21,10 @@ var ThemeChanger = {
|
|||
if (metaThemeColor) {
|
||||
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
|
||||
}
|
||||
},
|
||||
|
||||
setFontSize: function(fontSize) {
|
||||
document.documentElement.style.fontSize = fontSize ? (12 + fontSize * 2) + 'px' : '';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -139,21 +139,35 @@ Tip.createTips = function(container) {
|
|||
return;
|
||||
}
|
||||
container.find('[title]').each(function(ix, el) {
|
||||
var tip = new Tip($(el));
|
||||
tip.init();
|
||||
el._tip = tip;
|
||||
Tip.createTip(el);
|
||||
});
|
||||
};
|
||||
|
||||
Tip.createTip = function(el) {
|
||||
if (!Tip.enabled) {
|
||||
return;
|
||||
}
|
||||
var tip = new Tip($(el));
|
||||
tip.init();
|
||||
el._tip = tip;
|
||||
};
|
||||
|
||||
Tip.hideTips = function(container) {
|
||||
if (!Tip.enabled) {
|
||||
return;
|
||||
}
|
||||
container.find('[data-title]').each(function(ix, el) {
|
||||
if (el._tip) {
|
||||
el._tip.hide();
|
||||
}
|
||||
Tip.hideTip(el);
|
||||
});
|
||||
};
|
||||
|
||||
Tip.hideTip = function(el) {
|
||||
if (!Tip.enabled) {
|
||||
return;
|
||||
}
|
||||
if (el._tip) {
|
||||
el._tip.hide();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Tip;
|
||||
|
|
|
@ -8,6 +8,7 @@ var Backbone = require('backbone'),
|
|||
ListWrapView = require('../views/list-wrap-view'),
|
||||
DetailsView = require('../views/details/details-view'),
|
||||
GrpView = require('../views/grp-view'),
|
||||
TagView = require('../views/tag-view'),
|
||||
OpenView = require('../views/open-view'),
|
||||
SettingsView = require('../views/settings/settings-view'),
|
||||
KeyChangeView = require('../views/key-change-view'),
|
||||
|
@ -48,11 +49,13 @@ var AppView = Backbone.View.extend({
|
|||
this.views.details = new DetailsView();
|
||||
this.views.details.appModel = this.model;
|
||||
this.views.grp = new GrpView();
|
||||
this.views.tag = new TagView({ model: this.model });
|
||||
|
||||
this.views.menu.listenDrag(this.views.menuDrag);
|
||||
this.views.list.listenDrag(this.views.listDrag);
|
||||
|
||||
this.listenTo(this.model.settings, 'change:theme', this.setTheme);
|
||||
this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize);
|
||||
this.listenTo(this.model.files, 'update reset', this.fileListUpdated);
|
||||
|
||||
this.listenTo(Backbone, 'select-all', this.selectAll);
|
||||
|
@ -62,10 +65,12 @@ var AppView = Backbone.View.extend({
|
|||
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, 'key-change-pending', this.keyChangePending);
|
||||
this.listenTo(Backbone, 'toggle-settings', this.toggleSettings);
|
||||
this.listenTo(Backbone, 'toggle-menu', this.toggleMenu);
|
||||
this.listenTo(Backbone, 'toggle-details', this.toggleDetails);
|
||||
this.listenTo(Backbone, 'edit-group', this.editGroup);
|
||||
this.listenTo(Backbone, 'edit-tag', this.editTag);
|
||||
this.listenTo(Backbone, 'launcher-open-file', this.launcherOpenFile);
|
||||
this.listenTo(Backbone, 'user-idle', this.userIdle);
|
||||
this.listenTo(Backbone, 'app-minimized', this.appMinimized);
|
||||
|
@ -94,6 +99,7 @@ var AppView = Backbone.View.extend({
|
|||
this.views.listDrag.setElement(this.$el.find('.app__list-drag')).render();
|
||||
this.views.details.setElement(this.$el.find('.app__details')).render();
|
||||
this.views.grp.setElement(this.$el.find('.app__grp')).render().hide();
|
||||
this.views.tag.setElement(this.$el.find('.app__tag')).render().hide();
|
||||
this.showLastOpenFile();
|
||||
return this;
|
||||
},
|
||||
|
@ -106,6 +112,7 @@ var AppView = Backbone.View.extend({
|
|||
this.views.listDrag.hide();
|
||||
this.views.details.hide();
|
||||
this.views.grp.hide();
|
||||
this.views.tag.hide();
|
||||
this.views.footer.toggle(this.model.files.hasOpenFiles());
|
||||
this.hideSettings();
|
||||
this.hideOpenFile();
|
||||
|
@ -145,6 +152,7 @@ var AppView = Backbone.View.extend({
|
|||
this.views.listDrag.show();
|
||||
this.views.details.show();
|
||||
this.views.grp.hide();
|
||||
this.views.tag.hide();
|
||||
this.views.footer.show();
|
||||
this.hideOpenFile();
|
||||
this.hideSettings();
|
||||
|
@ -182,6 +190,7 @@ var AppView = Backbone.View.extend({
|
|||
this.views.listDrag.hide();
|
||||
this.views.details.hide();
|
||||
this.views.grp.hide();
|
||||
this.views.tag.hide();
|
||||
this.hideOpenFile();
|
||||
this.hideKeyChange();
|
||||
this.views.settings = new SettingsView({ model: this.model });
|
||||
|
@ -198,11 +207,24 @@ var AppView = Backbone.View.extend({
|
|||
this.views.list.hide();
|
||||
this.views.listDrag.hide();
|
||||
this.views.details.hide();
|
||||
this.views.tag.hide();
|
||||
this.views.grp.show();
|
||||
},
|
||||
|
||||
showKeyChange: function(file) {
|
||||
if (this.views.keyChange || Alerts.alertDisplayed) {
|
||||
showEditTag: function() {
|
||||
this.views.listWrap.hide();
|
||||
this.views.list.hide();
|
||||
this.views.listDrag.hide();
|
||||
this.views.details.hide();
|
||||
this.views.grp.hide();
|
||||
this.views.tag.show();
|
||||
},
|
||||
|
||||
showKeyChange: function(file, viewConfig) {
|
||||
if (Alerts.alertDisplayed) {
|
||||
return;
|
||||
}
|
||||
if (this.views.keyChange && this.views.keyChange.model.remote) {
|
||||
return;
|
||||
}
|
||||
this.hideSettings();
|
||||
|
@ -212,7 +234,10 @@ var AppView = Backbone.View.extend({
|
|||
this.views.listDrag.hide();
|
||||
this.views.details.hide();
|
||||
this.views.grp.hide();
|
||||
this.views.keyChange = new KeyChangeView({ model: file });
|
||||
this.views.tag.hide();
|
||||
this.views.keyChange = new KeyChangeView({
|
||||
model: { file: file, expired: viewConfig.expired, remote: viewConfig.remote }
|
||||
});
|
||||
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));
|
||||
|
@ -322,7 +347,7 @@ var AppView = Backbone.View.extend({
|
|||
|
||||
menuSelect: function(opt) {
|
||||
this.model.menu.select(opt);
|
||||
if (!this.views.grp.isHidden()) {
|
||||
if (!this.views.grp.isHidden() || !this.views.tag.isHidden()) {
|
||||
this.showEntries();
|
||||
}
|
||||
},
|
||||
|
@ -445,18 +470,31 @@ var AppView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
remoteKeyChanged: function(e) {
|
||||
this.showKeyChange(e.file);
|
||||
this.showKeyChange(e.file, { remote: true });
|
||||
},
|
||||
|
||||
keyChangePending: function(e) {
|
||||
this.showKeyChange(e.file, { expired: true });
|
||||
},
|
||||
|
||||
keyChangeAccept: function(e) {
|
||||
this.showEntries();
|
||||
this.model.syncFile(e.file, {
|
||||
remoteKey: {
|
||||
password: e.password,
|
||||
keyFileName: e.keyFileName,
|
||||
keyFileData: e.keyFileData
|
||||
if (e.expired) {
|
||||
e.file.setPassword(e.password);
|
||||
if (e.keyFileData && e.keyFileName) {
|
||||
e.file.setKeyFile(e.keyFileData, e.keyFileName);
|
||||
} else {
|
||||
e.file.removeKeyFile();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.model.syncFile(e.file, {
|
||||
remoteKey: {
|
||||
password: e.password,
|
||||
keyFileName: e.keyFileName,
|
||||
keyFileData: e.keyFileData
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleSettings: function(page) {
|
||||
|
@ -503,6 +541,15 @@ var AppView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
editTag: function(tag) {
|
||||
if (tag && this.views.tag.isHidden()) {
|
||||
this.showEditTag();
|
||||
this.views.tag.showTag(tag);
|
||||
} else {
|
||||
this.showEntries();
|
||||
}
|
||||
},
|
||||
|
||||
contextmenu: function(e) {
|
||||
if (['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) < 0) {
|
||||
e.preventDefault();
|
||||
|
@ -521,6 +568,10 @@ var AppView = Backbone.View.extend({
|
|||
ThemeChanger.setTheme(this.model.settings.get('theme'));
|
||||
},
|
||||
|
||||
setFontSize: function() {
|
||||
ThemeChanger.setFontSize(this.model.settings.get('fontSize'));
|
||||
},
|
||||
|
||||
extLinkClick: function(e) {
|
||||
if (Launcher) {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone'),
|
||||
FeatureDetector = require('../util/feature-detector'),
|
||||
Links = require('../const/links'),
|
||||
Timeouts = require('../const/timeouts');
|
||||
|
||||
var AutoTypeHintView = Backbone.View.extend({
|
||||
template: require('templates/auto-type-hint.hbs'),
|
||||
|
||||
events: {},
|
||||
|
||||
initialize: function(opts) {
|
||||
this.input = opts.input;
|
||||
this.bodyClick = this.bodyClick.bind(this);
|
||||
this.inputBlur = this.inputBlur.bind(this);
|
||||
$('body').on('click', this.bodyClick);
|
||||
this.input.addEventListener('blur', this.inputBlur);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.renderTemplate({
|
||||
cmd: FeatureDetector.isMac() ? 'command' : 'ctrl',
|
||||
hasCtrl: FeatureDetector.isMac(),
|
||||
link: Links.AutoType
|
||||
});
|
||||
var rect = this.input.getBoundingClientRect();
|
||||
this.$el.appendTo(document.body).css({
|
||||
left: rect.left, top: rect.bottom + 1, width: rect.width
|
||||
});
|
||||
var selfRect = this.$el[0].getBoundingClientRect();
|
||||
var bodyRect = document.body.getBoundingClientRect();
|
||||
if (selfRect.bottom > bodyRect.bottom) {
|
||||
this.$el.css('height', selfRect.height + bodyRect.bottom - selfRect.bottom - 1);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
$('body').off('click', this.bodyClick);
|
||||
this.input.removeEventListener('blur', this.inputBlur);
|
||||
Backbone.View.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
bodyClick: function(e) {
|
||||
if (this.removeTimer) {
|
||||
clearTimeout(this.removeTimer);
|
||||
this.removeTimer = null;
|
||||
}
|
||||
if (e.target === this.input) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if ($.contains(this.$el[0], e.target) || e.target === this.$el[0]) {
|
||||
e.stopPropagation();
|
||||
if (e.target.tagName.toLowerCase() === 'a' && !e.target.href) {
|
||||
var text = $(e.target).text();
|
||||
if (text[0] !== '{') {
|
||||
text = text.split(' ')[0];
|
||||
}
|
||||
this.insertText(text);
|
||||
}
|
||||
this.input.focus();
|
||||
} else {
|
||||
this.remove();
|
||||
}
|
||||
},
|
||||
|
||||
inputBlur: function() {
|
||||
if (!this.removeTimer) {
|
||||
this.removeTimer = setTimeout(this.remove.bind(this), Timeouts.DrobDownClickWait);
|
||||
}
|
||||
},
|
||||
|
||||
insertText: function(text) {
|
||||
var pos = this.input.selectionEnd || this.input.value.length;
|
||||
this.input.value = this.input.value.substr(0, pos) + text + this.input.value.substr(pos);
|
||||
this.input.selectionStart = this.input.selectionEnd = pos + text.length;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AutoTypeHintView;
|
|
@ -0,0 +1,77 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
var Backbone = require('backbone'),
|
||||
AutoTypeHintView = require('../auto-type-hint-view'),
|
||||
Locale = require('../../util/locale'),
|
||||
FeatureDetector = require('../../util/feature-detector'),
|
||||
AutoType = require('../../auto-type');
|
||||
|
||||
var DetailsAutoTypeView = Backbone.View.extend({
|
||||
template: require('templates/details/details-auto-type.hbs'),
|
||||
|
||||
events: {
|
||||
'focus #details__auto-type-sequence': 'seqFocus',
|
||||
'input #details__auto-type-sequence': 'seqInput',
|
||||
'keypress #details__auto-type-sequence': 'seqKeyPress',
|
||||
'keydown #details__auto-type-sequence': 'seqKeyDown',
|
||||
'change #details__auto-type-enabled': 'enabledChange',
|
||||
'change #details__auto-type-obfuscation': 'obfuscationChange'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.views = {};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var detAutoTypeShortcutsDesc = Locale.detAutoTypeShortcutsDesc
|
||||
.replace('{}', FeatureDetector.actionShortcutSymbol() + 'T')
|
||||
.replace('{}', FeatureDetector.globalShortcutSymbol() + 'T');
|
||||
this.renderTemplate({
|
||||
enabled: this.model.getEffectiveEnableAutoType(),
|
||||
obfuscation: this.model.autoTypeObfuscation,
|
||||
sequence: this.model.autoTypeSequence,
|
||||
windows: this.model.autoTypeWindows,
|
||||
defaultSequence: this.model.group.getEffectiveAutoTypeSeq(),
|
||||
detAutoTypeShortcutsDesc: detAutoTypeShortcutsDesc
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
seqInput: function(e) {
|
||||
var that = this;
|
||||
var el = e.target;
|
||||
var seq = $.trim(el.value);
|
||||
AutoType.validate(this.model, seq, function(err) {
|
||||
$(el).toggleClass('input--error', !!err);
|
||||
if (!err) {
|
||||
that.model.setAutoTypeSeq(seq);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
seqKeyPress: function(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
seqKeyDown: function(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
seqFocus: function(e) {
|
||||
if (!this.views.hint) {
|
||||
this.views.hint = new AutoTypeHintView({input: e.target}).render();
|
||||
this.views.hint.on('remove', (function() {delete this.views.hint; }).bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
enabledChange: function(e) {
|
||||
this.model.setEnableAutoType(e.target.checked);
|
||||
},
|
||||
|
||||
obfuscationChange: function(e) {
|
||||
this.model.setAutoTypeObfuscation(e.target.checked);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DetailsAutoTypeView;
|
|
@ -6,6 +6,7 @@ var Backbone = require('backbone'),
|
|||
AppSettingsModel = require('../../models/app-settings-model'),
|
||||
Scrollable = require('../../mixins/scrollable'),
|
||||
FieldViewText = require('../fields/field-view-text'),
|
||||
FieldViewSelect = require('../fields/field-view-select'),
|
||||
FieldViewAutocomplete = require('../fields/field-view-autocomplete'),
|
||||
FieldViewDate = require('../fields/field-view-date'),
|
||||
FieldViewTags = require('../fields/field-view-tags'),
|
||||
|
@ -18,12 +19,14 @@ var Backbone = require('backbone'),
|
|||
DetailsHistoryView = require('./details-history-view'),
|
||||
DetailsAttachmentView = require('./details-attachment-view'),
|
||||
DetailsAddFieldView = require('./details-add-field-view'),
|
||||
DetailsAutoTypeView = require('./details-auto-type-view'),
|
||||
DropdownView = require('../../views/dropdown-view'),
|
||||
Keys = require('../../const/keys'),
|
||||
KeyHandler = require('../../comp/key-handler'),
|
||||
Alerts = require('../../comp/alerts'),
|
||||
CopyPaste = require('../../comp/copy-paste'),
|
||||
OtpQrReqder = require('../../comp/otp-qr-reader'),
|
||||
AutoType = require('../../auto-type'),
|
||||
Format = require('../../util/format'),
|
||||
Locale = require('../../util/locale'),
|
||||
Tip = require('../../util/tip'),
|
||||
|
@ -51,6 +54,8 @@ var DetailsView = Backbone.View.extend({
|
|||
'click .details__buttons-trash': 'moveToTrash',
|
||||
'click .details__buttons-trash-del': 'deleteFromTrash',
|
||||
'click .details__back-button': 'backClick',
|
||||
'click .details__attachment-add': 'attachmentBtnClick',
|
||||
'change .details__attachment-input-file': 'attachmentFileChange',
|
||||
'dragover .details': 'dragover',
|
||||
'dragleave .details': 'dragleave',
|
||||
'drop .details': 'drop'
|
||||
|
@ -62,6 +67,7 @@ var DetailsView = Backbone.View.extend({
|
|||
this.initScroll();
|
||||
this.listenTo(Backbone, 'select-entry', this.showEntry);
|
||||
this.listenTo(Backbone, 'copy-password', this.copyPassword);
|
||||
this.listenTo(Backbone, 'auto-type', this.autoTypeGlobal);
|
||||
this.listenTo(Backbone, 'copy-user', this.copyUserName);
|
||||
this.listenTo(Backbone, 'copy-url', this.copyUrl);
|
||||
this.listenTo(OtpQrReqder, 'qr-read', this.otpCodeRead);
|
||||
|
@ -69,6 +75,7 @@ var DetailsView = Backbone.View.extend({
|
|||
KeyHandler.onKey(Keys.DOM_VK_C, this.copyPassword, this, KeyHandler.SHORTCUT_ACTION, false, true);
|
||||
KeyHandler.onKey(Keys.DOM_VK_B, this.copyUserName, this, KeyHandler.SHORTCUT_ACTION, false, true);
|
||||
KeyHandler.onKey(Keys.DOM_VK_U, this.copyUrl, this, KeyHandler.SHORTCUT_ACTION, false, true);
|
||||
KeyHandler.onKey(Keys.DOM_VK_T, this.autoType, this, KeyHandler.SHORTCUT_ACTION);
|
||||
KeyHandler.onKey(Keys.DOM_VK_DELETE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
|
||||
KeyHandler.onKey(Keys.DOM_VK_BACK_SPACE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
|
||||
},
|
||||
|
@ -95,14 +102,7 @@ var DetailsView = Backbone.View.extend({
|
|||
render: function () {
|
||||
this.removeScroll();
|
||||
this.removeFieldViews();
|
||||
if (this.views.sub) {
|
||||
this.views.sub.remove();
|
||||
delete this.views.sub;
|
||||
}
|
||||
if (this.views.dropdownView) {
|
||||
this.views.dropdownView.remove();
|
||||
delete this.views.dropdownView;
|
||||
}
|
||||
this.removeInnerViews();
|
||||
if (!this.model) {
|
||||
this.$el.html(this.emptyTemplate());
|
||||
return;
|
||||
|
@ -135,6 +135,17 @@ var DetailsView = Backbone.View.extend({
|
|||
|
||||
addFieldViews: function() {
|
||||
var model = this.model;
|
||||
if (model.isJustCreated && this.appModel.files.length > 1) {
|
||||
var fileNames = this.appModel.files.map(function(file) {
|
||||
return { id: file.id, value: file.get('name'), selected: file === this.model.file };
|
||||
}, this);
|
||||
this.fileEditView = new FieldViewSelect({ model: { name: '$File', title: Locale.detFile,
|
||||
value: function() { return fileNames; } } });
|
||||
this.fieldViews.push(this.fileEditView);
|
||||
} else {
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: Locale.detFile,
|
||||
value: function() { return model.fileName; } } }));
|
||||
}
|
||||
this.userEditView = new FieldViewAutocomplete({ model: { name: '$UserName', title: Locale.detUser,
|
||||
value: function() { return model.user; }, getCompletions: this.getUserNameCompletions.bind(this) } });
|
||||
this.fieldViews.push(this.userEditView);
|
||||
|
@ -150,8 +161,8 @@ var DetailsView = Backbone.View.extend({
|
|||
value: function() { return model.tags; } } }));
|
||||
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: Locale.detFile,
|
||||
value: function() { return model.fileName; } } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Group', title: Locale.detGroup,
|
||||
value: function() { return model.groupName; }, tip: function() { return model.getGroupPath().join(' / '); } } }));
|
||||
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: Locale.detUpdated,
|
||||
|
@ -239,6 +250,9 @@ var DetailsView = Backbone.View.extend({
|
|||
moreOptions.push({value: 'toggle-empty', icon: 'eye-slash', text: Locale.detMenuHideEmpty});
|
||||
}
|
||||
moreOptions.push({value: 'otp', icon: 'clock-o', text: Locale.detSetupOtp});
|
||||
if (AutoType.enabled) {
|
||||
moreOptions.push({value: 'auto-type', icon: 'keyboard-o', text: Locale.detAutoType});
|
||||
}
|
||||
var rect = this.moreView.labelEl[0].getBoundingClientRect();
|
||||
dropdownView.render({
|
||||
position: {top: rect.bottom, left: rect.left},
|
||||
|
@ -264,6 +278,9 @@ var DetailsView = Backbone.View.extend({
|
|||
case 'otp':
|
||||
this.setupOtp();
|
||||
break;
|
||||
case 'auto-type':
|
||||
this.toggleAutoType();
|
||||
break;
|
||||
default:
|
||||
if (e.item.lastIndexOf('add:', 0) === 0) {
|
||||
var fieldName = e.item.substr(4);
|
||||
|
@ -454,6 +471,13 @@ var DetailsView = Backbone.View.extend({
|
|||
this.model.setField(fieldName, e.val);
|
||||
this.entryUpdated();
|
||||
return;
|
||||
} else if (fieldName === 'File') {
|
||||
var newFile = this.appModel.files.get(e.val);
|
||||
this.model.moveToFile(newFile);
|
||||
this.appModel.activeEntryId = this.model.id;
|
||||
this.entryUpdated();
|
||||
Backbone.trigger('select-entry', this.model);
|
||||
return;
|
||||
} else if (fieldName) {
|
||||
this.model.setField(fieldName, e.val);
|
||||
}
|
||||
|
@ -562,6 +586,18 @@ var DetailsView = Backbone.View.extend({
|
|||
this.$el.find('.details').removeClass('details--drag');
|
||||
this.dragging = false;
|
||||
var files = e.target.files || e.originalEvent.dataTransfer.files;
|
||||
this.addAttachedFiles(files);
|
||||
},
|
||||
|
||||
attachmentBtnClick: function() {
|
||||
this.$el.find('.details__attachment-input-file')[0].click();
|
||||
},
|
||||
|
||||
attachmentFileChange: function(e) {
|
||||
this.addAttachedFiles(e.target.files);
|
||||
},
|
||||
|
||||
addAttachedFiles: function(files) {
|
||||
_.forEach(files, function(file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = (function() {
|
||||
|
@ -738,6 +774,32 @@ var DetailsView = Backbone.View.extend({
|
|||
fieldView.edit();
|
||||
this.fieldViews.push(fieldView);
|
||||
}
|
||||
},
|
||||
|
||||
toggleAutoType: function() {
|
||||
if (this.views.autoType) {
|
||||
this.views.autoType.remove();
|
||||
delete this.views.autoType;
|
||||
return;
|
||||
}
|
||||
this.views.autoType = new DetailsAutoTypeView({
|
||||
el: this.$el.find('.details__body-after'),
|
||||
model: this.model
|
||||
}).render();
|
||||
},
|
||||
|
||||
autoType: function() {
|
||||
var entry = this.model;
|
||||
// AutoType.getActiveWindowTitle(function() {
|
||||
// console.log(arguments);
|
||||
// });
|
||||
AutoType.hideWindow(function() {
|
||||
AutoType.run(entry);
|
||||
});
|
||||
},
|
||||
|
||||
autoTypeGlobal: function() {
|
||||
// TODO
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -35,8 +35,9 @@ var DropdownView = Backbone.View.extend({
|
|||
|
||||
itemClick: function(e) {
|
||||
e.stopPropagation();
|
||||
var selected = $(e.target).closest('.dropdown__item').data('value');
|
||||
this.trigger('select', { item: selected });
|
||||
var el = $(e.target).closest('.dropdown__item');
|
||||
var selected = el.data('value');
|
||||
this.trigger('select', { item: selected, el: el });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -21,7 +21,9 @@ var FieldViewAutocomplete = FieldViewText.extend({
|
|||
width: fieldRect.width - 2
|
||||
});
|
||||
this.autocomplete.mousedown(this.autocompleteClick.bind(this));
|
||||
if (!this.input.val()) {
|
||||
if (this.input.val()) {
|
||||
this.autocomplete.hide();
|
||||
} else {
|
||||
this.updateAutocomplete();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
var FieldView = require('./field-view');
|
||||
|
||||
var FieldViewSelect = FieldView.extend({
|
||||
readonly: true,
|
||||
|
||||
renderValue: function(value) {
|
||||
return '<select>' +
|
||||
value.map(function(opt) {
|
||||
return '<option ' + 'value="' + _.escape(opt.id) + '" ' + (opt.selected ? 'selected ' : '') + '>' +
|
||||
_.escape(opt.value) +
|
||||
'</option>';
|
||||
}).join('') +
|
||||
'</select>';
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var that = this;
|
||||
FieldView.prototype.render.call(this);
|
||||
this.valueEl.addClass('details__field-value--select');
|
||||
this.valueEl.find('select:first').change(function(e) {
|
||||
that.triggerChange({ val: e.target.value, field: that.model.name });
|
||||
});
|
||||
},
|
||||
|
||||
fieldLabelClick: function() {},
|
||||
|
||||
fieldValueClick: function() {},
|
||||
|
||||
edit: function() {},
|
||||
|
||||
startEdit: function() {},
|
||||
|
||||
endEdit: function(newVal, extra) {
|
||||
if (!this.editing) {
|
||||
return;
|
||||
}
|
||||
delete this.input;
|
||||
FieldView.prototype.endEdit.call(this, newVal, extra);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = FieldViewSelect;
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
var Backbone = require('backbone'),
|
||||
FeatureDetector = require('../../util/feature-detector'),
|
||||
CopyPaste = require('../../comp/copy-paste');
|
||||
CopyPaste = require('../../comp/copy-paste'),
|
||||
Tip = require('../../util/tip');
|
||||
|
||||
var FieldView = Backbone.View.extend({
|
||||
template: require('templates/details/field.hbs'),
|
||||
|
@ -12,16 +13,30 @@ var FieldView = Backbone.View.extend({
|
|||
'click .details__field-value': 'fieldValueClick'
|
||||
},
|
||||
|
||||
render: 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,
|
||||
canEditTitle: this.model.newField, protect: this.value && this.value.isProtected });
|
||||
this.valueEl = this.$el.find('.details__field-value');
|
||||
this.valueEl.html(this.renderValue(this.value));
|
||||
this.labelEl = this.$el.find('.details__field-label');
|
||||
if (this.model.tip) {
|
||||
this.tip = typeof this.model.tip === 'function' ? this.model.tip() : this.model.tip;
|
||||
if (this.tip) {
|
||||
this.valueEl.attr('title', this.tip);
|
||||
Tip.createTip(this.valueEl);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
if (this.tip) {
|
||||
Tip.hideTip(this.valueEl);
|
||||
}
|
||||
Backbone.View.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
update: function() {
|
||||
if (typeof this.model.value === 'function') {
|
||||
var newVal = this.model.value();
|
||||
|
@ -70,7 +85,7 @@ var FieldView = Backbone.View.extend({
|
|||
range.selectNodeContents(this.valueEl[0]);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
copyRes = CopyPaste.copy(this.valueEl.text());
|
||||
copyRes = CopyPaste.copy(this.valueEl[0].innerText || this.valueEl.text());
|
||||
if (copyRes) {
|
||||
selection.removeAllRanges();
|
||||
this.trigger('copy', { source: this, copyRes: copyRes });
|
||||
|
@ -121,11 +136,15 @@ var FieldView = Backbone.View.extend({
|
|||
arg = extra;
|
||||
}
|
||||
if (arg) {
|
||||
arg.sender = this;
|
||||
this.trigger('change', arg);
|
||||
this.triggerChange(arg);
|
||||
}
|
||||
this.valueEl.html(this.renderValue(this.value));
|
||||
this.$el.removeClass('details__field--edit');
|
||||
},
|
||||
|
||||
triggerChange: function(arg) {
|
||||
arg.sender = this;
|
||||
this.trigger('change', arg);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
var Backbone = require('backbone'),
|
||||
Scrollable = require('../mixins/scrollable'),
|
||||
IconSelectView = require('./icon-select-view');
|
||||
IconSelectView = require('./icon-select-view'),
|
||||
AutoTypeHintView = require('./auto-type-hint-view'),
|
||||
AutoType = require('../auto-type');
|
||||
|
||||
var GrpView = Backbone.View.extend({
|
||||
template: require('templates/grp.hbs'),
|
||||
|
@ -12,7 +14,10 @@ var GrpView = Backbone.View.extend({
|
|||
'click .grp__buttons-trash': 'moveToTrash',
|
||||
'click .grp__back-button': 'returnToApp',
|
||||
'input #grp__field-title': 'changeTitle',
|
||||
'change #grp__check-search': 'setEnableSearching'
|
||||
'focus #grp__field-auto-type-seq': 'focusAutoTypeSeq',
|
||||
'input #grp__field-auto-type-seq': 'changeAutoTypeSeq',
|
||||
'change #grp__check-search': 'setEnableSearching',
|
||||
'change #grp__check-auto-type': 'setEnableAutoType'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
@ -26,8 +31,12 @@ var GrpView = Backbone.View.extend({
|
|||
title: this.model.get('title'),
|
||||
icon: this.model.get('icon') || 'folder',
|
||||
customIcon: this.model.get('customIcon'),
|
||||
enableSearching: this.model.get('enableSearching') !== false,
|
||||
readonly: this.model.get('top')
|
||||
enableSearching: this.model.getEffectiveEnableSearching(),
|
||||
readonly: this.model.get('top'),
|
||||
canAutoType: AutoType.enabled,
|
||||
autoTypeSeq: this.model.get('autoTypeSeq'),
|
||||
autoTypeEnabled: this.model.getEffectiveEnableAutoType(),
|
||||
defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq()
|
||||
}, { plain: true });
|
||||
if (!this.model.get('title')) {
|
||||
this.$el.find('#grp__field-title').focus();
|
||||
|
@ -68,6 +77,25 @@ var GrpView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
changeAutoTypeSeq: function(e) {
|
||||
var that = this;
|
||||
var el = e.target;
|
||||
var seq = $.trim(el.value);
|
||||
AutoType.validate(null, seq, function(err) {
|
||||
$(e.target).toggleClass('input--error', !!err);
|
||||
if (!err) {
|
||||
that.model.setAutoTypeSeq(seq);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
focusAutoTypeSeq: function(e) {
|
||||
if (!this.views.hint) {
|
||||
this.views.hint = new AutoTypeHintView({input: e.target}).render();
|
||||
this.views.hint.on('remove', (function() {delete this.views.hint; }).bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
showIconsSelect: function() {
|
||||
if (this.views.sub) {
|
||||
this.removeSubView();
|
||||
|
@ -107,6 +135,11 @@ var GrpView = Backbone.View.extend({
|
|||
this.model.setEnableSearching(enabled);
|
||||
},
|
||||
|
||||
setEnableAutoType: function(e) {
|
||||
var enabled = e.target.checked;
|
||||
this.model.setEnableAutoType(enabled);
|
||||
},
|
||||
|
||||
returnToApp: function() {
|
||||
Backbone.trigger('edit-group');
|
||||
}
|
||||
|
|
|
@ -25,11 +25,13 @@ var KeyChangeView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
this.keyFileName = this.model.get('keyFileName') || null;
|
||||
this.keyFileName = this.model.file.get('keyFileName') || null;
|
||||
this.keyFileData = null;
|
||||
this.renderTemplate({
|
||||
fileName: this.model.get('name'),
|
||||
keyFileName: this.model.get('keyFileName')
|
||||
fileName: this.model.file.get('name'),
|
||||
keyFileName: this.model.file.get('keyFileName'),
|
||||
title: this.model.expired ? Locale.keyChangeTitleExpired : Locale.keyChangeTitleRemote,
|
||||
message: this.model.expired ? Locale.keyChangeMessageExpired : Locale.keyChangeMessageRemote
|
||||
});
|
||||
this.$el.find('.key-change__keyfile-name').text(this.keyFileName ? ': ' + this.keyFileName : '');
|
||||
this.inputEl = this.$el.find('.key-change__pass');
|
||||
|
@ -81,7 +83,8 @@ var KeyChangeView = Backbone.View.extend({
|
|||
|
||||
accept: function() {
|
||||
this.trigger('accept', {
|
||||
file: this.model,
|
||||
file: this.model.file,
|
||||
expired: this.model.expired,
|
||||
password: this.passwordInput.value,
|
||||
keyFileName: this.keyFileName,
|
||||
keyFileData: this.keyFileData
|
||||
|
|
|
@ -4,9 +4,12 @@ var Backbone = require('backbone'),
|
|||
Resizable = require('../mixins/resizable'),
|
||||
Scrollable = require('../mixins/scrollable'),
|
||||
ListSearchView = require('./list-search-view'),
|
||||
DropdownView = require('./dropdown-view'),
|
||||
EntryPresenter = require('../presenters/entry-presenter'),
|
||||
DragDropInfo = require('../comp/drag-drop-info'),
|
||||
AppSettingsModel = require('../models/app-settings-model');
|
||||
AppSettingsModel = require('../models/app-settings-model'),
|
||||
Locale = require('../util/locale'),
|
||||
Format = require('../util/format');
|
||||
|
||||
var ListView = Backbone.View.extend({
|
||||
template: require('templates/list.hbs'),
|
||||
|
@ -14,6 +17,7 @@ var ListView = Backbone.View.extend({
|
|||
|
||||
events: {
|
||||
'click .list__item': 'itemClick',
|
||||
'click .list__table-options': 'tableOptionsClick',
|
||||
'dragstart .list__item': 'itemDragStart'
|
||||
},
|
||||
|
||||
|
@ -26,6 +30,17 @@ var ListView = Backbone.View.extend({
|
|||
|
||||
itemsEl: null,
|
||||
|
||||
|
||||
tableColumns: [
|
||||
{ val: 'title', name: 'title', enabled: true },
|
||||
{ val: 'user', name: 'user', enabled: true },
|
||||
{ val: 'url', name: 'website', enabled: true },
|
||||
{ val: 'tags', name: 'tags', enabled: true },
|
||||
{ val: 'notes', name: 'notes', enabled: true },
|
||||
{ val: 'groupName', name: 'group', enabled: false },
|
||||
{ val: 'fileName', name: 'file', enabled: false }
|
||||
],
|
||||
|
||||
initialize: function () {
|
||||
this.initScroll();
|
||||
this.views = {};
|
||||
|
@ -64,12 +79,19 @@ var ListView = Backbone.View.extend({
|
|||
var itemsTemplate = this.getItemsTemplate();
|
||||
var noColor = AppSettingsModel.instance.get('colorfulIcons') ? '' : 'grayscale';
|
||||
var presenter = new EntryPresenter(this.getDescField(), noColor, this.model.activeEntryId);
|
||||
var columns = {};
|
||||
this.tableColumns.forEach(function(col) {
|
||||
if (col.enabled) {
|
||||
columns[col.val] = true;
|
||||
}
|
||||
});
|
||||
presenter.columns = columns;
|
||||
var itemsHtml = '';
|
||||
this.items.forEach(function (item) {
|
||||
presenter.present(item);
|
||||
itemsHtml += itemTemplate(presenter);
|
||||
}, this);
|
||||
var html = itemsTemplate({ items: itemsHtml });
|
||||
var html = itemsTemplate({ items: itemsHtml, columns: this.tableColumns });
|
||||
this.itemsEl.html(html);
|
||||
} else {
|
||||
this.itemsEl.html(this.emptyTemplate());
|
||||
|
@ -206,6 +228,47 @@ var ListView = Backbone.View.extend({
|
|||
e.originalEvent.dataTransfer.setData('text/entry', id);
|
||||
e.originalEvent.dataTransfer.effectAllowed = 'move';
|
||||
DragDropInfo.dragObject = this.items.get(id);
|
||||
},
|
||||
|
||||
tableOptionsClick: function(e) {
|
||||
e.stopImmediatePropagation();
|
||||
if (this.views.optionsDropdown) {
|
||||
this.hideOptionsDropdown();
|
||||
return;
|
||||
}
|
||||
var view = new DropdownView();
|
||||
this.listenTo(view, 'cancel', this.hideOptionsDropdown);
|
||||
this.listenTo(view, 'select', this.optionsDropdownSelect);
|
||||
var targetElRect = this.$el.find('.list__table-options')[0].getBoundingClientRect();
|
||||
var options = this.tableColumns.map(function(col) {
|
||||
return {
|
||||
value: col.val,
|
||||
icon: col.enabled ? 'check-square-o' : 'square-o',
|
||||
text: Format.capFirst(Locale[col.name])
|
||||
};
|
||||
});
|
||||
view.render({
|
||||
position: {
|
||||
top: targetElRect.bottom,
|
||||
left: targetElRect.left
|
||||
},
|
||||
options: options
|
||||
});
|
||||
this.views.optionsDropdown = view;
|
||||
},
|
||||
|
||||
hideOptionsDropdown: function() {
|
||||
if (this.views.optionsDropdown) {
|
||||
this.views.optionsDropdown.remove();
|
||||
delete this.views.optionsDropdown;
|
||||
}
|
||||
},
|
||||
|
||||
optionsDropdownSelect: function(e) {
|
||||
var col = _.find(this.tableColumns, function(c) { return c.val === e.item; });
|
||||
col.enabled = !col.enabled;
|
||||
e.el.find('i:first').toggleClass('fa-check-square-o fa-square-o');
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ var MenuItemView = Backbone.View.extend({
|
|||
render: function() {
|
||||
this.removeInnerViews();
|
||||
this.renderTemplate(this.model.attributes);
|
||||
this.iconEl = this.$el.find('i');
|
||||
this.iconEl = this.$el.find('i.menu__item-icon');
|
||||
var items = this.model.get('items');
|
||||
if (items) {
|
||||
items.forEach(function (item) {
|
||||
|
@ -156,7 +156,14 @@ var MenuItemView = Backbone.View.extend({
|
|||
editItem: function(e) {
|
||||
if (this.model.get('active') && this.model.get('editable')) {
|
||||
e.stopPropagation();
|
||||
Backbone.trigger('edit-group', this.model);
|
||||
switch (this.model.get('filterKey')) {
|
||||
case 'tag':
|
||||
Backbone.trigger('edit-tag', this.model);
|
||||
break;
|
||||
case 'group':
|
||||
Backbone.trigger('edit-group', this.model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -173,7 +180,8 @@ var MenuItemView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
dropAllowed: function(e) {
|
||||
return ['text/group', 'text/entry'].indexOf(e.originalEvent.dataTransfer.types[0]) >= 0;
|
||||
var types = e.originalEvent.dataTransfer.types;
|
||||
return types.indexOf('text/group') >= 0 || types.indexOf('text/entry') >= 0;
|
||||
},
|
||||
|
||||
dragstart: function(e) {
|
||||
|
|
|
@ -7,6 +7,7 @@ var Backbone = require('backbone'),
|
|||
Alerts = require('../comp/alerts'),
|
||||
SecureInput = require('../comp/secure-input'),
|
||||
DropboxLink = require('../comp/dropbox-link'),
|
||||
FeatureDetector = require('../util/feature-detector'),
|
||||
Logger = require('../util/logger'),
|
||||
Locale = require('../util/locale'),
|
||||
UrlUtil = require('../util/url-util'),
|
||||
|
@ -82,6 +83,12 @@ var OpenView = Backbone.View.extend({
|
|||
return this;
|
||||
},
|
||||
|
||||
focusInput: function() {
|
||||
if (!FeatureDetector.isMobile()) {
|
||||
this.inputEl.focus();
|
||||
}
|
||||
},
|
||||
|
||||
getLastOpenFiles: function() {
|
||||
return this.model.fileInfos.map(function(f) {
|
||||
var icon = 'file-text';
|
||||
|
@ -123,7 +130,7 @@ var OpenView = Backbone.View.extend({
|
|||
esc: '',
|
||||
enter: '',
|
||||
success: function(res) {
|
||||
that.inputEl.focus();
|
||||
that.focusInput();
|
||||
if (res === 'skip') {
|
||||
that.model.settings.set('skipOpenLocalWarn', true);
|
||||
}
|
||||
|
@ -220,13 +227,13 @@ var OpenView = Backbone.View.extend({
|
|||
this.$el.find('.open__settings-key-file').removeClass('hide');
|
||||
this.inputEl[0].removeAttribute('readonly');
|
||||
this.inputEl[0].setAttribute('placeholder', Locale.openPassFor + ' ' + this.params.name);
|
||||
this.inputEl.focus();
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
displayOpenKeyFile: function() {
|
||||
this.$el.toggleClass('open--key-file', !!this.params.keyFileName);
|
||||
this.$el.find('.open__settings-key-file-name').text(this.params.keyFileName || Locale.openKeyFile);
|
||||
this.inputEl.focus();
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
setFile: function(file, keyFile, fileReadyCallback) {
|
||||
|
@ -442,6 +449,10 @@ var OpenView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
openDb: function() {
|
||||
if (this.params.id && this.model.files.get(this.params.id)) {
|
||||
this.trigger('close');
|
||||
return;
|
||||
}
|
||||
if (this.busy || !this.params.name) {
|
||||
return;
|
||||
}
|
||||
|
@ -458,7 +469,7 @@ var OpenView = Backbone.View.extend({
|
|||
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err);
|
||||
if (err) {
|
||||
logger.error('Error opening file', err);
|
||||
this.inputEl.focus();
|
||||
this.focusInput();
|
||||
this.inputEl[0].selectionStart = 0;
|
||||
this.inputEl[0].selectionEnd = this.inputEl.val().length;
|
||||
if (err.code !== 'InvalidKey') {
|
||||
|
@ -603,7 +614,7 @@ var OpenView = Backbone.View.extend({
|
|||
}
|
||||
this.$el.find('.open__pass-area').removeClass('hide');
|
||||
this.$el.find('.open__config').addClass('hide');
|
||||
this.inputEl.focus();
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
applyConfig: function(config) {
|
||||
|
|
|
@ -34,7 +34,8 @@ var SettingsFileView = Backbone.View.extend({
|
|||
'change #settings__file-trash': 'changeTrash',
|
||||
'input #settings__file-hist-len': 'changeHistoryLength',
|
||||
'input #settings__file-hist-size': 'changeHistorySize',
|
||||
'input #settings__file-key-rounds': 'changeKeyRounds'
|
||||
'input #settings__file-key-rounds': 'changeKeyRounds',
|
||||
'input #settings__file-key-change-force': 'changeKeyChangeForce'
|
||||
},
|
||||
|
||||
appModel: null,
|
||||
|
@ -69,6 +70,7 @@ var SettingsFileView = Backbone.View.extend({
|
|||
historyMaxItems: this.model.get('historyMaxItems'),
|
||||
historyMaxSize: Math.round(this.model.get('historyMaxSize') / 1024 / 1024),
|
||||
keyEncryptionRounds: this.model.get('keyEncryptionRounds'),
|
||||
keyChangeForce: this.model.get('keyChangeForce') > 0 ? this.model.get('keyChangeForce') : null,
|
||||
storageProviders: storageProviders
|
||||
});
|
||||
if (!this.model.get('created')) {
|
||||
|
@ -383,6 +385,14 @@ var SettingsFileView = Backbone.View.extend({
|
|||
return;
|
||||
}
|
||||
this.model.setKeyEncryptionRounds(value);
|
||||
},
|
||||
|
||||
changeKeyChangeForce: function(e) {
|
||||
var value = Math.round(e.target.value);
|
||||
if (isNaN(value) || value <= 0) {
|
||||
value = -1;
|
||||
}
|
||||
this.model.setKeyChange(true, value);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
var Backbone = require('backbone'),
|
||||
SettingsPrvView = require('./settings-prv-view'),
|
||||
SettingsLogsView = require('./settings-logs-view'),
|
||||
Launcher = require('../../comp/launcher'),
|
||||
Updater = require('../../comp/updater'),
|
||||
Format = require('../../util/format'),
|
||||
|
@ -19,6 +20,7 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
|
||||
events: {
|
||||
'change .settings__general-theme': 'changeTheme',
|
||||
'change .settings__general-font-size': 'changeFontSize',
|
||||
'change .settings__general-expand': 'changeExpandGroups',
|
||||
'change .settings__general-auto-update': 'changeAutoUpdate',
|
||||
'change .settings__general-idle-minutes': 'changeIdleMinutes',
|
||||
|
@ -35,19 +37,23 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
'click .settings__general-download-update-btn': 'downloadUpdate',
|
||||
'click .settings__general-update-found-btn': 'installFoundUpdate',
|
||||
'change .settings__general-prv-check': 'changeStorageEnabled',
|
||||
'click .settings__general-show-advanced': 'showAdvancedSettings',
|
||||
'click .settings__general-dev-tools-link': 'openDevTools',
|
||||
'click .settings__general-try-beta-link': 'tryBeta'
|
||||
'click .settings__general-try-beta-link': 'tryBeta',
|
||||
'click .settings__general-show-logs-link': 'showLogs'
|
||||
},
|
||||
|
||||
views: {},
|
||||
views: null,
|
||||
|
||||
allThemes: {
|
||||
fb: 'Flat blue',
|
||||
db: 'Dark brown',
|
||||
wh: 'White'
|
||||
fb: Locale.setGenThemeFb,
|
||||
db: Locale.setGenThemeDb,
|
||||
wh: Locale.setGenThemeWh,
|
||||
hc: Locale.setGenThemeHc
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.views = {};
|
||||
this.listenTo(UpdateModel.instance, 'change:status', this.render, this);
|
||||
this.listenTo(UpdateModel.instance, 'change:updateStatus', this.render, this);
|
||||
},
|
||||
|
@ -60,6 +66,7 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
this.renderTemplate({
|
||||
themes: this.allThemes,
|
||||
activeTheme: AppSettingsModel.instance.get('theme'),
|
||||
fontSize: AppSettingsModel.instance.get('fontSize'),
|
||||
expandGroups: AppSettingsModel.instance.get('expandGroups'),
|
||||
canClearClipboard: !!Launcher,
|
||||
clipboardSeconds: AppSettingsModel.instance.get('clipboardSeconds'),
|
||||
|
@ -163,6 +170,11 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
AppSettingsModel.instance.set('theme', theme);
|
||||
},
|
||||
|
||||
changeFontSize: function(e) {
|
||||
var fontSize = +e.target.value;
|
||||
AppSettingsModel.instance.set('fontSize', fontSize);
|
||||
},
|
||||
|
||||
changeClipboard: function(e) {
|
||||
var clipboardSeconds = +e.target.value;
|
||||
AppSettingsModel.instance.set('clipboardSeconds', clipboardSeconds);
|
||||
|
@ -252,12 +264,17 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
changeStorageEnabled: function(e) {
|
||||
var storage = Storage[$(e.target).data('storage')];
|
||||
if (storage) {
|
||||
storage.enabled = e.target.checked;
|
||||
storage.setEnabled(e.target.checked);
|
||||
AppSettingsModel.instance.set(storage.name, storage.enabled);
|
||||
this.$el.find('.settings__general-' + storage.name).toggleClass('hide', !e.target.checked);
|
||||
}
|
||||
},
|
||||
|
||||
showAdvancedSettings: function() {
|
||||
this.$el.find('.settings__general-show-advanced, .settings__general-advanced').toggleClass('hide');
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
openDevTools: function() {
|
||||
if (Launcher) {
|
||||
Launcher.openDevTools();
|
||||
|
@ -273,6 +290,18 @@ var SettingsGeneralView = Backbone.View.extend({
|
|||
} else {
|
||||
location.href = Links.BetaWebApp;
|
||||
}
|
||||
},
|
||||
|
||||
showLogs: function() {
|
||||
if (this.views.logView) {
|
||||
this.views.logView.remove();
|
||||
}
|
||||
this.views.logView = new SettingsLogsView({ el: this.$el.find('.settings__general-advanced') }).render();
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
scrollToBottom: function() {
|
||||
this.$el.closest('.scroller').scrollTop(this.$el.height());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone'),
|
||||
Logger = require('../../util/logger'),
|
||||
Format = require('../../util/format');
|
||||
|
||||
var SettingsLogView = Backbone.View.extend({
|
||||
template: require('templates/settings/settings-logs-view.hbs'),
|
||||
|
||||
render: function() {
|
||||
var logs = Logger.getLast().map(function(item) {
|
||||
return {
|
||||
level: item.level,
|
||||
msg: '[' + Format.padStr(item.level.toUpperCase(), 5) + '] ' + item.args.join(' ')
|
||||
};
|
||||
});
|
||||
this.renderTemplate({ logs: logs });
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SettingsLogView;
|
|
@ -13,7 +13,8 @@ var SettingsShortcutsView = Backbone.View.extend({
|
|||
alt: FeatureDetector.altShortcutSymbol(true),
|
||||
global: FeatureDetector.globalShortcutSymbol(true),
|
||||
globalIsLarge: FeatureDetector.globalShortcutIsLarge(),
|
||||
globalShortcutsSupported: !!Launcher
|
||||
globalShortcutsSupported: !!Launcher,
|
||||
autoTypeSupported: !!Launcher
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
'use strict';
|
||||
|
||||
var Backbone = require('backbone'),
|
||||
Locale = require('../util/locale'),
|
||||
Alerts = require('../comp/alerts');
|
||||
|
||||
var TagView = Backbone.View.extend({
|
||||
template: require('templates/tag.hbs'),
|
||||
|
||||
events: {
|
||||
'click .tag__buttons-trash': 'moveToTrash',
|
||||
'click .tag__back-button': 'returnToApp',
|
||||
'click .tag__btn-rename': 'renameTag'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.appModel = this.model;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.model) {
|
||||
this.renderTemplate({
|
||||
title: this.model.get('title')
|
||||
}, { plain: true });
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
showTag: function(tag) {
|
||||
this.model = tag;
|
||||
this.render();
|
||||
},
|
||||
|
||||
renameTag: function() {
|
||||
var title = $.trim(this.$el.find('#tag__field-title').val());
|
||||
if (!title || title === this.model.get('title')) {
|
||||
return;
|
||||
}
|
||||
if (/[;,:]/.test(title)) {
|
||||
Alerts.error({ header: Locale.tagBadName, body: Locale.tagBadNameBody });
|
||||
return;
|
||||
}
|
||||
if (this.appModel.tags.some(function(t) { return t.toLowerCase() === title.toLowerCase(); })) {
|
||||
Alerts.error({ header: Locale.tagExists, body: Locale.tagExistsBody });
|
||||
return;
|
||||
}
|
||||
this.appModel.renameTag(this.model.get('title'), title);
|
||||
Backbone.trigger('select-all');
|
||||
},
|
||||
|
||||
moveToTrash: function() {
|
||||
this.title = null;
|
||||
var that = this;
|
||||
Alerts.yesno({
|
||||
header: Locale.tagTrashQuestion,
|
||||
body: Locale.tagTrashQuestionBody,
|
||||
success: function() {
|
||||
that.appModel.renameTag(that.model.get('title'), undefined);
|
||||
Backbone.trigger('select-all');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
returnToApp: function() {
|
||||
Backbone.trigger('edit-tag');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TagView;
|
|
@ -87,7 +87,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__grp {
|
||||
&__grp, &__tag {
|
||||
@include flex(1);
|
||||
@include display(flex);
|
||||
overflow: hidden;
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
@include flex(1);
|
||||
@include display(flex);
|
||||
@include align-items(stretch);
|
||||
@include align-content(flex-start);
|
||||
@include flex-direction(row);
|
||||
@include justify-content(flex-start);
|
||||
@include flex-wrap(wrap);
|
||||
|
@ -147,6 +148,10 @@
|
|||
@include flex-direction(column);
|
||||
@include justify-content(flex-start);
|
||||
}
|
||||
|
||||
&-after {
|
||||
@include flex(100% 1 0);
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
|
@ -203,8 +208,9 @@
|
|||
border-radius: $base-border-radius;
|
||||
&:hover {
|
||||
transition: border-color $base-duration $base-timing;
|
||||
border: 1px solid;
|
||||
@include th {
|
||||
border: 1px solid light-border-color();
|
||||
border-color: light-border-color();
|
||||
box-shadow: 0 0 3px form-box-shadow-color();
|
||||
}
|
||||
.details__field-value-add-label {
|
||||
|
@ -218,7 +224,6 @@
|
|||
}
|
||||
.details__field--multiline & {
|
||||
width: 0;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.details__field--edit &,
|
||||
|
@ -241,6 +246,10 @@
|
|||
line-height: 1.5em;
|
||||
overflow: hidden;
|
||||
}
|
||||
>label {
|
||||
font-weight: normal;
|
||||
@include user-select(none);
|
||||
}
|
||||
.details__body-aside & {
|
||||
@include th { color: muted-color(); }
|
||||
a { @include th { color: muted-color(); } }
|
||||
|
@ -268,6 +277,16 @@
|
|||
&:before { content: $fa-var-unlock; }
|
||||
.details__field--protected & { &:before { content: $fa-var-lock; } }
|
||||
}
|
||||
&--select {
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
.details__field--editable:hover & { border-width: 0; }
|
||||
}
|
||||
>select {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
padding: 0 $base-padding-h;
|
||||
}
|
||||
}
|
||||
|
||||
&--no-select {
|
||||
|
@ -318,15 +337,13 @@
|
|||
text-align: center;
|
||||
overflow: hidden;
|
||||
transition: color $base-duration $base-timing;
|
||||
display: none;
|
||||
@include nomobile { display: block; }
|
||||
&:hover {
|
||||
@include th { color: medium-color(); }
|
||||
}
|
||||
&-title {
|
||||
display: none;
|
||||
transition: color $slow-transition-out;
|
||||
margin-right: .4em;
|
||||
margin-right: $base-padding-h;
|
||||
color: transparent;
|
||||
.details__attachment-add:hover & {
|
||||
display: inline;
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
text-align: center;
|
||||
}
|
||||
&__body {
|
||||
@include flex(0);
|
||||
@include display(flex);
|
||||
@include align-items(flex-start);
|
||||
@include flex-direction(column);
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
&-icon-search {
|
||||
@include th { color: muted-color(); }
|
||||
position: absolute;
|
||||
top: .6em;
|
||||
top: .5em;
|
||||
right: .5em;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
|
@ -91,15 +91,27 @@
|
|||
}
|
||||
|
||||
&__table {
|
||||
border-collapse: collapse;
|
||||
width: calc(100% - 2px);
|
||||
td, th {
|
||||
padding: $base-padding;
|
||||
text-align: left;
|
||||
&:first-child {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
th:first-child {
|
||||
padding: 0;
|
||||
width: 3em;
|
||||
}
|
||||
&-options {
|
||||
@include icon-btn();
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding: $base-padding-px;
|
||||
padding: 6px 10px 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
@ -111,7 +123,7 @@
|
|||
}
|
||||
|
||||
&:not(.list__item--table) {
|
||||
height: 32px;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
&--expired {
|
||||
|
|
|
@ -57,6 +57,17 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
&-collapse {
|
||||
display: none;
|
||||
.menu__item--collapsed>& {
|
||||
display: block;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
@include position(absolute, 50% null null 1em);
|
||||
@include transform(translateY(-50%));
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
@include area-selectable();
|
||||
padding: $base-padding;
|
||||
|
|
|
@ -34,6 +34,13 @@
|
|||
@include size(1em);
|
||||
}
|
||||
}
|
||||
@include mobile() {
|
||||
.open__icons--lower & {
|
||||
margin: 8px;
|
||||
&-i, &-svg { font-size: 2em; }
|
||||
&-text { font-size: .5em; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__pass {
|
||||
|
|
|
@ -114,4 +114,16 @@
|
|||
&__general-prv {
|
||||
margin-bottom: $base-padding-v;
|
||||
}
|
||||
&__logs {
|
||||
@include user-select(text);
|
||||
margin-top: $base-padding-v;
|
||||
&-log {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
&--debug { color: $white; opacity: .8; }
|
||||
&--info { color: $white; }
|
||||
&--warn { color: $yellow; }
|
||||
&--error { color: $red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.tag {
|
||||
@include flex(1);
|
||||
@include display(flex);
|
||||
@include align-items(stretch);
|
||||
@include flex-direction(column);
|
||||
@include justify-content(flex-start);
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
|
||||
&__back-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: $base-padding-h;
|
||||
padding: $base-padding-v * 2 0 1px 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__space {
|
||||
@include flex(1);
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
@include display(flex);
|
||||
@include flex-direction(row);
|
||||
margin-top: $base-padding-v;
|
||||
|
||||
&-trash {
|
||||
@include icon-btn($error:true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,5 +12,4 @@
|
|||
@import "buttons";
|
||||
@import "forms";
|
||||
@import "lists";
|
||||
@import "tables";
|
||||
@import "typography";
|
||||
|
|
|
@ -6,8 +6,10 @@ html {
|
|||
|
||||
body {
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
cursor: default;
|
||||
position: fixed;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
noscript {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-family: $base-font-family;
|
||||
font-size: $base-font-size;
|
||||
font-size: 1rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
|
|
|
@ -16,24 +16,22 @@ $violet: #d946db;
|
|||
@function error-color() { @return th(error-color); }
|
||||
|
||||
// Text
|
||||
@function muted-color() { @return mix(medium-color(), background-color(), 30%); }
|
||||
@function muted-color-border() { @return mix(medium-color(), background-color(), 15%); }
|
||||
@function muted-color() { @return mix(medium-color(), background-color(), th(mute-percent)); }
|
||||
@function muted-color-border() { @return mix(medium-color(), background-color(), th(mute-percent) / 2); }
|
||||
@function text-selection-bg-color() { @return rgba(action-color(), .3); }
|
||||
@function text-selection-bg-color-error() { @return rgba(error-color(), .8); }
|
||||
@function text-contrast-color($bg) { @if (lightness($bg) >= lightness(background-color())) { @return text-color(); } @else { @return background-color(); } }
|
||||
|
||||
// Borders, shadows
|
||||
|
||||
@function base-border-color() { @return mix(medium-color(), background-color(), 50%); }
|
||||
@function accent-border-color() { @return mix(medium-color(), background-color(), 65%); }
|
||||
@function light-border-color() { @return mix(medium-color(), background-color(), 10%); }
|
||||
@function light-border-color() { @return mix(medium-color(), background-color(), th(light-border-percent)); }
|
||||
@function form-box-shadow-color() { @return rgba($black, 0.06); }
|
||||
@function form-box-shadow-color-focus() { @return adjust-color(action-color(), $lightness: -5%, $alpha: -0.3); }
|
||||
@function form-box-shadow-color-focus-error() { @return adjust-color(error-color(), $lightness: -5%, $alpha: -0.3); }
|
||||
@function dropdown-box-shadow-color() { @return rgba(medium-color(), .05); }
|
||||
|
||||
// Backgrounds
|
||||
|
||||
@function secondary-background-color() { @return mix(medium-color(), background-color(), 10%); }
|
||||
@function intermediate-background-color() { @return mix(medium-color(), background-color(), 3%); }
|
||||
@function intermediate-pressed-background-color() { @return mix(medium-color(), background-color(), 2.6%); }
|
||||
|
|
|
@ -16,13 +16,13 @@ input,
|
|||
select {
|
||||
display: block;
|
||||
font-family: $base-font-family;
|
||||
font-size: $base-font-size;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
font-family: $base-font-family;
|
||||
font-size: $base-font-size;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: $small-spacing / 2;
|
||||
|
||||
|
@ -39,7 +39,7 @@ input[type=text], input[type=password], textarea, input:not([type]) {
|
|||
border-radius: $base-border-radius;
|
||||
box-sizing: border-box;
|
||||
font-family: $base-font-family;
|
||||
font-size: $base-font-size;
|
||||
font-size: 1rem;
|
||||
margin-bottom: $small-spacing;
|
||||
padding: $base-spacing / 3;
|
||||
transition: border-color $base-duration $base-timing;
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
//table {
|
||||
// border-collapse: collapse;
|
||||
// font-feature-settings: "kern", "liga", "tnum";
|
||||
// margin: $small-spacing 0;
|
||||
// table-layout: fixed;
|
||||
// width: 100%;
|
||||
//}
|
||||
//
|
||||
//th {
|
||||
// @include th { border-bottom: 1px solid light-border-color(); }
|
||||
// font-weight: 600;
|
||||
// padding: $small-spacing 0;
|
||||
// text-align: left;
|
||||
//}
|
||||
//
|
||||
//td {
|
||||
// @include th { border-bottom: base-border(); }
|
||||
// padding: $small-spacing 0;
|
||||
//}
|
||||
//
|
||||
//tr,
|
||||
//td,
|
||||
//th {
|
||||
// vertical-align: middle;
|
||||
//}
|
|
@ -1,3 +1,7 @@
|
|||
html {
|
||||
font-size: $base-font-size;
|
||||
}
|
||||
|
||||
body {
|
||||
@include size(100%);
|
||||
@include user-select(none);
|
||||
|
@ -5,12 +9,10 @@ body {
|
|||
color: text-color();
|
||||
background-color: background-color();
|
||||
}
|
||||
overflow: auto;
|
||||
font-family: $base-font-family;
|
||||
font-feature-settings: "kern", "liga";
|
||||
font-size: $base-font-size;
|
||||
font-feature-settings: "kern", "liga 0";
|
||||
font-size: 1rem;
|
||||
line-height: $base-line-height;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
|
@ -28,13 +30,13 @@ h6 {
|
|||
margin: 0 0 $small-spacing;
|
||||
}
|
||||
|
||||
$small-header-font-size: modular-scale(2, $base-font-size);
|
||||
$large-header-font-size: modular-scale(3, $base-font-size);
|
||||
$small-header-font-size: modular-scale(2, 1rem);
|
||||
$large-header-font-size: modular-scale(3, 1rem);
|
||||
|
||||
h6 { font-size: $base-font-size; }
|
||||
h5 { font-size: modular-scale(1, $base-font-size, 1.1); }
|
||||
h4 { font-size: modular-scale(2, $base-font-size, 1.1); }
|
||||
h3 { font-size: modular-scale(3, $base-font-size, 1.1); }
|
||||
h6 { font-size: 1rem; }
|
||||
h5 { font-size: modular-scale(1, 1rem, 1.1); }
|
||||
h4 { font-size: modular-scale(2, 1rem, 1.1); }
|
||||
h3 { font-size: modular-scale(3, 1rem, 1.1); }
|
||||
h2 { font-size: $small-header-font-size; }
|
||||
h1 { font-size: $large-header-font-size; }
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.modal {
|
||||
@include position(absolute, 0 null null 0);
|
||||
@include size(100%);
|
||||
@include th { background-color: rgba(background-color(), .9); }
|
||||
@include th { background-color: rgba(background-color(), th(modal-opacity)); }
|
||||
z-index: $z-index-modal;
|
||||
|
||||
transition: background-color $base-duration $base-timing;
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
|
||||
@import "base/base";
|
||||
|
||||
@import "utils/drag";
|
||||
@import "utils/common-dropdown";
|
||||
@import "utils/auto-type-hint";
|
||||
@import "utils/drag";
|
||||
@import "utils/selection";
|
||||
|
||||
@import "common/dates";
|
||||
|
@ -24,6 +25,7 @@
|
|||
@import "areas/details";
|
||||
@import "areas/footer";
|
||||
@import "areas/grp";
|
||||
@import "areas/tag";
|
||||
@import "areas/generator";
|
||||
@import "areas/key-change";
|
||||
@import "areas/list";
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
$themes: ();
|
||||
|
||||
@import "theme-defaults";
|
||||
@import "dark-brown";
|
||||
@import "flat-blue";
|
||||
@import "white";
|
||||
@import "high-contrast";
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
$themes: map-merge($themes, (
|
||||
db: (
|
||||
db: map-merge($theme-defaults, (
|
||||
background-color: #342F2E,
|
||||
medium-color: #FED9D8,
|
||||
text-color: #FFEAE9,
|
||||
action-color: #2C9957,
|
||||
error-color: #FD6D67,
|
||||
)
|
||||
))
|
||||
));
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
$themes: map-merge($themes, (
|
||||
fb: (
|
||||
fb: map-merge($theme-defaults, (
|
||||
background-color: #282C34,
|
||||
medium-color: #ABB2BF,
|
||||
text-color: #D7DAE0,
|
||||
action-color: #528BFF,
|
||||
error-color: #C34034,
|
||||
)
|
||||
))
|
||||
));
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
$themes: map-merge($themes, (
|
||||
hc: map-merge($theme-defaults, (
|
||||
background-color: #FAFAFA,
|
||||
medium-color: #050505,
|
||||
text-color: #050505,
|
||||
action-color: #2d72d7,
|
||||
error-color: #e74859,
|
||||
mute-percent: 60%,
|
||||
light-border-percent: 50%,
|
||||
modal-opacity: 1,
|
||||
))
|
||||
));
|
|
@ -0,0 +1,5 @@
|
|||
$theme-defaults: (
|
||||
mute-percent: 30%,
|
||||
light-border-percent: 10%,
|
||||
modal-opacity: .9,
|
||||
)
|
|
@ -1,9 +1,9 @@
|
|||
$themes: map-merge($themes, (
|
||||
wh: (
|
||||
wh: map-merge($theme-defaults, (
|
||||
background-color: #FAFAFA,
|
||||
medium-color: #050505,
|
||||
text-color: #424243,
|
||||
action-color: #475FD7,
|
||||
error-color: #E75675,
|
||||
)
|
||||
))
|
||||
));
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
.auto-type-hint {
|
||||
@include common-dropdown;
|
||||
position: absolute;
|
||||
z-index: $z-index-no-modal;
|
||||
border-radius: $base-border-radius;
|
||||
padding: $base-padding;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
&__block {
|
||||
margin-bottom: $base-padding-v;
|
||||
>a, >b {
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin-right: $base-padding-h;
|
||||
margin-bottom: $base-padding-v;
|
||||
}
|
||||
}
|
||||
&__link-details {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: $base-padding;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
<div class="app__details"></div>
|
||||
</div>
|
||||
<div class="app__grp"></div>
|
||||
<div class="app__tag"></div>
|
||||
</div>
|
||||
<div class="app__footer"></div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<div class="auto-type-hint">
|
||||
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
|
||||
<div class="auto-type-hint__block">
|
||||
<div>{{res 'autoTypeEntryFields'}}:</div>
|
||||
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
|
||||
<a>{TOTP}</a><a>{S:Custom Field Name}</a>
|
||||
</div>
|
||||
<div class="auto-type-hint__block">
|
||||
<div>{{res 'autoTypeModifiers'}}:</div>
|
||||
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
|
||||
</div>
|
||||
<div class="auto-type-hint__block">
|
||||
<div>{{res 'autoTypeKeys'}}:</div>
|
||||
<a>{TAB}</a><a>{ENTER}</a><a>{SPACE}</a><a>{UP}</a><a>{DOWN}</a><a>{LEFT}</a><a>{RIGHT}</a><a>{HOME}</a><a>{END}</a>
|
||||
<a>{+}</a><a>{%}</a><a>{^}</a><a>{~}</a><a>{(}</a><a>{)}</a><a>{[}</a><a>{]}</a><a>\{{}</a><a>{}}</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,38 @@
|
|||
<div class="details__auto-type">
|
||||
<div class="details__field">
|
||||
<div class="details__field-label">{{res 'detAutoType'}}</div>
|
||||
<div class="details__field-value">
|
||||
<input type="checkbox" class="input-base" id="details__auto-type-enabled" {{#if enabled}}checked{{/if}} />
|
||||
<label for="details__auto-type-enabled">{{res 'detAutoTypeEnabled'}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details__field">
|
||||
<div class="details__field-label">{{res 'detAutoTypeSequence'}}</div>
|
||||
<div class="details__field-value">
|
||||
<input type="text" id="details__auto-type-sequence" maxlength="1024"
|
||||
value="{{sequence}}" placeholder="{{defaultSequence}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="details__field">
|
||||
<div class="details__field-label">{{res 'detAutoTypeInput'}}</div>
|
||||
<div class="details__field-value">
|
||||
<input type="checkbox" class="input-base" id="details__auto-type-obfuscation" {{#if obfuscation}}checked{{/if}} />
|
||||
<label for="details__auto-type-obfuscation">{{res 'detAutoTypeObfuscation'}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details__field">
|
||||
<div class="details__field-label">{{res 'detAutoTypeShortcuts'}}</div>
|
||||
<div class="details__field-value">{{{detAutoTypeShortcutsDesc}}}</div>
|
||||
</div>
|
||||
{{!--{{#each windows as |win|}}
|
||||
<div class="details__field">
|
||||
<div class="details__field-label">{{res 'detAutoTypeWindow'}}</div>
|
||||
<div class="details__field-value details__auto-type-windows">
|
||||
<input type="text" maxlength="1024" class="details__auto-type-window-title"
|
||||
value="{{win.window}}" placeholder="{{res 'detAutoTypeInputWindow'}}" />
|
||||
<input type="text" maxlength="1024" class="details__auto-type-window-sequence"
|
||||
value="{{win.sequence}}" placeholder="{{../defaultSequence}}" />
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}--}}
|
||||
</div>
|
|
@ -26,6 +26,7 @@
|
|||
</div>
|
||||
<div class="details__body-aside">
|
||||
</div>
|
||||
<div class="details__body-after"></div>
|
||||
</div>
|
||||
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
|
||||
</div>
|
||||
|
@ -36,6 +37,12 @@
|
|||
<i class="details__buttons-trash fa fa-trash-o" title="{{res 'detDelEntry'}}" tip-placement="top"></i>
|
||||
{{~/if~}}
|
||||
<div class="details__attachments">
|
||||
<input type="file" class="details__attachment-input-file hide-by-pos" multiple />
|
||||
{{#ifneq attachments.length 0}}
|
||||
<div class="details__attachment-add">
|
||||
<i class="fa fa-paperclip"></i>
|
||||
</div>
|
||||
{{/ifneq}}
|
||||
{{#each attachments as |attachment ix|}}
|
||||
<div class="details__attachment" data-id="{{ix}}"><i class="fa fa-{{attachment.icon}}"></i> {{attachment.title}}</div>
|
||||
{{else}}
|
||||
|
|
|
@ -22,11 +22,24 @@
|
|||
<i class="fa fa-{{icon}} grp__icon"></i>
|
||||
{{/if}}
|
||||
<div class="grp__icons"></div>
|
||||
{{#if canAutoType}}
|
||||
{{#unless readonly}}
|
||||
<div>
|
||||
<input type="checkbox" class="input-base" id="grp__check-auto-type" {{#if autoTypeEnabled}}checked{{/if}} />
|
||||
<label for="grp__check-auto-type">{{res 'grpAutoType'}}</label>
|
||||
</div>
|
||||
{{/unless}}
|
||||
<div class="grp__field">
|
||||
<label for="grp__field-auto-type-seq">{{res 'grpAutoTypeSeq'}}:</label>
|
||||
<input type="text" class="input-base" id="grp__field-auto-type-seq" value="{{autoTypeSeq}}"
|
||||
size="50" maxlength="1024" placeholder="{{res 'grpAutoTypeSeqDefault'}}: {{defaultAutoTypeSeq}}" />
|
||||
</div>
|
||||
{{/if}}
|
||||
</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>
|
||||
<i class="grp__buttons-trash fa fa-trash-o" title="{{res 'grpTrash'}}" tip-placement="right"></i>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<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__header">{{fileName}}: {{title}}</div>
|
||||
<div class="key-change__body">
|
||||
<div class="key-change__message">{{res 'keyChangeMessage'}}:</div>
|
||||
<div class="key-change__message">{{message}}:</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 />
|
||||
<input class="key-change__pass" type="password" size="30" autocomplete="off" maxlength="1024" autofocus />
|
||||
<div class="key-change__keyfile">
|
||||
<i class="fa fa-key"></i> {{res 'openKeyFile'}}<span class="key-change__keyfile-name"></span>
|
||||
</div>
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
<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>
|
||||
{{#if columns.title}}<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>{{/if}}
|
||||
{{#if columns.user}}<td>{{user}}</td>{{/if}}
|
||||
{{#if columns.url}}<td>{{url}}</td>{{/if}}
|
||||
{{#if columns.tags}}<td>{{tags}}</td>{{/if}}
|
||||
{{#if columns.notes}}<td>{{notes}}</td>{{/if}}
|
||||
{{#if columns.groupName}}<td>{{groupName}}</td>{{/if}}
|
||||
{{#if columns.fileName}}<td>{{fileName}}</td>{{/if}}
|
||||
</tr>
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
</div>
|
||||
<div class="list__search-field-wrap">
|
||||
<input type="text" class="list__search-field input-padding-right" autocomplete="off">
|
||||
<i class="list__search-icon-search fa fa-search" title="{{res 'searchAdvTitle'}}"></i>
|
||||
<div class="list__search-icon-search" title="{{res 'searchAdvTitle'}}">
|
||||
<i class="fa fa-search"></i>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list__search-btn-new" title="{{res 'searchAddNew'}}">
|
||||
<i class="fa fa-plus"></i>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<table class="list__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{Res 'title'}}</th>
|
||||
<th>{{Res 'user'}}</th>
|
||||
<th>{{Res 'website'}}</th>
|
||||
<th>{{Res 'tags'}}</th>
|
||||
<th>{{Res 'notes'}}</th>
|
||||
<th><i class="fa fa-bars muted-color list__table-options"></i></th>
|
||||
{{#each columns as |col|}}
|
||||
{{#if col.enabled}}<th>{{Res col.name}}</th>{{/if}}
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
{{~#if options.length}} menu__item--with-options {{/if~}}
|
||||
{{~#if cls}} {{cls}}{{/if~}}
|
||||
">
|
||||
{{#if collapsible}}<i class="menu__item-collapse fa fa-ellipsis-v muted-color" title="{{res 'menuItemCollapsed'}}"></i>{{/if}}
|
||||
<div class="menu__item-body" {{#if drag}}draggable="true"{{/if}}>
|
||||
{{#if customIcon~}}
|
||||
<img src="{{{customIcon}}}" class="menu__item-icon menu__item-icon--image" />
|
||||
|
|
|
@ -48,8 +48,8 @@
|
|||
<div class="open__pass-warning muted-color invisible"><i class="fa fa-exclamation-triangle"></i> {{res 'openCaps'}}</div>
|
||||
</div>
|
||||
<div class="open__pass-field-wrap">
|
||||
<input class="open__pass-input" type="password" size="30" autocomplete="off" maxlength="128"
|
||||
placeholder="Click to open a file" readonly />
|
||||
<input class="open__pass-input" type="password" size="30" autocomplete="off" maxlength="1024"
|
||||
placeholder="{{res 'openClickToOpen'}}" readonly />
|
||||
<div class="open__pass-enter-btn"><i class="fa fa-level-down fa-rotate-90"></i></div>
|
||||
<div class="open__pass-opening-icon"><i class="fa fa-spinner fa-spin"></i></div>
|
||||
</div>
|
||||
|
|
|
@ -19,12 +19,10 @@
|
|||
|
||||
<h3>Core components</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/antelle/kdbxweb" target="_blank">kdbxweb</a><span class="muted-color">, web kdbx library</span></li>
|
||||
<li><a href="https://github.com/keeweb/kdbxweb" target="_blank">kdbxweb</a><span class="muted-color">, web kdbx library</span></li>
|
||||
<li><a href="https://github.com/vibornoff/asmcrypto.js/" target="_blank">asmcrypto</a><span class="muted-color">, JavaScript cryptographic library
|
||||
with performance in mind</span></li>
|
||||
<li><a href="http://nodeca.github.io/pako/" target="_blank">pako</a><span class="muted-color">, zlib port to JavaScript, very fast</span></li>
|
||||
<li><a href="https://github.com/jindw/xmldom" target="_blank">xmldom</a><span class="muted-color">, a pure js W3C standard based DOMParser
|
||||
and XMLSerializer</span></li>
|
||||
</ul>
|
||||
|
||||
<h3>UI components</h3>
|
||||
|
|
|
@ -73,4 +73,7 @@
|
|||
<h2>{{res 'setFileAdvanced'}}</h2>
|
||||
<label for="settings__file-key-rounds">{{res 'setFileRounds'}}:</label>
|
||||
<input type="text" pattern="\d+" required class="settings__input input-base" id="settings__file-key-rounds" value="{{keyEncryptionRounds}}" />
|
||||
|
||||
<label for="settings__file-key-rounds">{{res 'setFileKeyChangeForce'}}:</label>
|
||||
<input type="text" pattern="\d*" class="settings__input input-base" id="settings__file-key-change-force" value="{{keyChangeForce}}" />
|
||||
</div>
|
||||
|
|
|
@ -45,6 +45,14 @@
|
|||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="settings__general-font-size">{{res 'setGenFontSize'}}:</label>
|
||||
<select class="settings__general-font-size settings__select input-base" id="settings__general-font-size">
|
||||
<option value="0" {{#ifeq fontSize 0}}selected{{/ifeq}}>{{res 'setGenFontSizeNormal'}}</option>
|
||||
<option value="1" {{#ifeq fontSize 1}}selected{{/ifeq}}>{{res 'setGenFontSizeLarge'}}</option>
|
||||
<option value="2" {{#ifeq fontSize 2}}selected{{/ifeq}}>{{res 'setGenFontSizeLargest'}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" class="settings__input input-base settings__general-expand" id="settings__general-expand" {{#if expandGroups}}checked{{/if}} />
|
||||
<label for="settings__general-expand">{{res 'setGenShowSubgroups'}}</label>
|
||||
|
@ -120,9 +128,13 @@
|
|||
<div class="settings__general-prv-wrap settings__general-{{prv.name}} {{#ifeq prv.enabled false}}hide{{/ifeq}}"></div>
|
||||
{{/each}}
|
||||
|
||||
{{#if devTools}}
|
||||
<h2>{{res 'setGenAdvanced'}}</h2>
|
||||
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button>
|
||||
<button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button>
|
||||
{{/if}}
|
||||
<a class="settings__general-show-advanced">{{res 'setGenShowAdvanced'}}</a>
|
||||
<div class="settings__general-advanced hide">
|
||||
{{#if devTools}}
|
||||
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button>
|
||||
<button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button>
|
||||
{{/if}}
|
||||
<button class="btn-silent settings__general-show-logs-link">{{res 'setGenShowAppLogs'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<div class="settings__logs">
|
||||
{{#each logs as |log|}}
|
||||
<pre class="settings__logs-log settings__logs-log--{{level}}">{{msg}}</pre>
|
||||
{{/each}}
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue