mirror of
https://github.com/keeweb/keeweb.git
synced 2024-06-27 07:45:08 +02:00
Merge branch 'develop'
This commit is contained in:
commit
186eb7b359
16
Gruntfile.js
16
Gruntfile.js
|
@ -17,7 +17,7 @@ module.exports = function(grunt) {
|
||||||
var pkg = require('./package.json');
|
var pkg = require('./package.json');
|
||||||
var dt = new Date().toISOString().replace(/T.*/, '');
|
var dt = new Date().toISOString().replace(/T.*/, '');
|
||||||
var electronVersion = '0.36.4';
|
var electronVersion = '0.36.4';
|
||||||
var appUpdateMinVersion = '0.5.0';
|
var minElectronVersionForUpdate = '0.32.0';
|
||||||
|
|
||||||
function replaceFont(css) {
|
function replaceFont(css) {
|
||||||
css.walkAtRules('font-face', function (rule) {
|
css.walkAtRules('font-face', function (rule) {
|
||||||
|
@ -73,6 +73,11 @@ module.exports = function(grunt) {
|
||||||
dest: 'tmp/favicon.png',
|
dest: 'tmp/favicon.png',
|
||||||
nonull: true
|
nonull: true
|
||||||
},
|
},
|
||||||
|
touchicon: {
|
||||||
|
src: 'app/touchicon.png',
|
||||||
|
dest: 'tmp/touchicon.png',
|
||||||
|
nonull: true
|
||||||
|
},
|
||||||
fonts: {
|
fonts: {
|
||||||
src: 'bower_components/font-awesome/fonts/fontawesome-webfont.*',
|
src: 'bower_components/font-awesome/fonts/fontawesome-webfont.*',
|
||||||
dest: 'tmp/fonts/',
|
dest: 'tmp/fonts/',
|
||||||
|
@ -154,7 +159,7 @@ module.exports = function(grunt) {
|
||||||
options: {
|
options: {
|
||||||
replacements: [
|
replacements: [
|
||||||
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version },
|
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version },
|
||||||
{ pattern: '# updmin:v0.0.0', replacement: '# updmin:v' + appUpdateMinVersion }
|
{ pattern: '# updmin:v0.0.0', replacement: '# updmin:v' + minElectronVersionForUpdate }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
|
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
|
||||||
|
@ -172,7 +177,7 @@ module.exports = function(grunt) {
|
||||||
js: {
|
js: {
|
||||||
entry: {
|
entry: {
|
||||||
app: 'app',
|
app: 'app',
|
||||||
vendor: ['zepto', 'jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'dropbox', 'pikaday', 'filesaver']
|
vendor: ['jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'dropbox', 'pikaday', 'filesaver']
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: 'tmp/js',
|
path: 'tmp/js',
|
||||||
|
@ -191,8 +196,7 @@ module.exports = function(grunt) {
|
||||||
backbone: 'backbone/backbone-min.js',
|
backbone: 'backbone/backbone-min.js',
|
||||||
underscore: 'underscore/underscore-min.js',
|
underscore: 'underscore/underscore-min.js',
|
||||||
_: 'underscore/underscore-min.js',
|
_: 'underscore/underscore-min.js',
|
||||||
zepto: 'zepto/zepto.min.js',
|
jquery: 'jquery/dist/jquery.min.js',
|
||||||
jquery: 'zepto/zepto.min.js',
|
|
||||||
hbs: 'handlebars/runtime.js',
|
hbs: 'handlebars/runtime.js',
|
||||||
kdbxweb: 'kdbxweb/dist/kdbxweb.js',
|
kdbxweb: 'kdbxweb/dist/kdbxweb.js',
|
||||||
dropbox: 'dropbox/lib/dropbox.min.js',
|
dropbox: 'dropbox/lib/dropbox.min.js',
|
||||||
|
@ -213,7 +217,6 @@ module.exports = function(grunt) {
|
||||||
{ pattern: /@@DATE/g, replacement: function() { return dt; } },
|
{ pattern: /@@DATE/g, replacement: function() { return dt; } },
|
||||||
{ pattern: /@@COMMIT/g, replacement: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); } }
|
{ pattern: /@@COMMIT/g, replacement: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); } }
|
||||||
]})},
|
]})},
|
||||||
{ test: /zepto(\.min)?\.js$/, loader: 'exports?Zepto; delete window.$; delete window.Zepto;' },
|
|
||||||
{ test: /baron(\.min)?\.js$/, loader: 'exports?baron; delete window.baron;' },
|
{ test: /baron(\.min)?\.js$/, loader: 'exports?baron; delete window.baron;' },
|
||||||
{ test: /pikadat\.js$/, loader: 'uglify' },
|
{ test: /pikadat\.js$/, loader: 'uglify' },
|
||||||
{ test: /handlebars/, loader: 'strip-sourcemap-loader' }
|
{ test: /handlebars/, loader: 'strip-sourcemap-loader' }
|
||||||
|
@ -373,6 +376,7 @@ module.exports = function(grunt) {
|
||||||
'jshint',
|
'jshint',
|
||||||
'copy:html',
|
'copy:html',
|
||||||
'copy:favicon',
|
'copy:favicon',
|
||||||
|
'copy:touchicon',
|
||||||
'copy:fonts',
|
'copy:fonts',
|
||||||
'webpack',
|
'webpack',
|
||||||
'uglify',
|
'uglify',
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
This webapp is a browser and desktop password manager compatible with KeePass databases. It doesn't require any server or additional resources.
|
This webapp is a browser and desktop password manager compatible with KeePass databases. It doesn't require any server or additional resources.
|
||||||
The app can run either in browser, or as a desktop app.
|
The app can run either in browser, or as a desktop app.
|
||||||
|
|
||||||
![screenshot](https://habrastorage.org/files/bfb/51e/d8d/bfb51ed8d19847d8afb827c4fbff7dd5.png)
|
![screenshot](https://habrastorage.org/files/ec9/108/3de/ec91083de3e64574a504bc438d038dec.png)
|
||||||
|
|
||||||
# Quick Links
|
# Quick Links
|
||||||
|
|
||||||
|
@ -15,8 +15,7 @@ Twitter: [kee_web](https://twitter.com/kee_web)
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
|
|
||||||
The app is already rather stable but might still need polishing, testing and improvements before v1 release, which is expected to happen in Feb 2016.
|
Project roadmap with planned features and approximate schedule is on [TODO](https://github.com/antelle/keeweb/wiki/TODO) page.
|
||||||
Please see [TODO](https://github.com/antelle/keeweb/wiki/TODO) for more details.
|
|
||||||
|
|
||||||
# Self-hosting
|
# Self-hosting
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<title>KeeWeb</title>
|
<title>KeeWeb</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<link rel="shortcut icon" href="favicon.png?__inline=true" />
|
<link rel="shortcut icon" href="favicon.png?__inline=true" />
|
||||||
|
<link rel="apple-touch-icon" sizes="192x192" href="touchicon.png?__inline=true">
|
||||||
<link rel="stylesheet" href="css/main.css?__inline=true" />
|
<link rel="stylesheet" href="css/main.css?__inline=true" />
|
||||||
<script src="js/vendor.js?__inline=true"></script>
|
<script src="js/vendor.js?__inline=true"></script>
|
||||||
<script src="js/app.js?__inline=true"></script>
|
<script src="js/app.js?__inline=true"></script>
|
||||||
|
|
|
@ -5,14 +5,26 @@ var FeatureDetector = require('../util/feature-detector'),
|
||||||
AppSettingsModel = require('../models/app-settings-model');
|
AppSettingsModel = require('../models/app-settings-model');
|
||||||
|
|
||||||
var CopyPaste = {
|
var CopyPaste = {
|
||||||
tryCopy: function() {
|
simpleCopy: !!Launcher,
|
||||||
try {
|
|
||||||
var success = document.execCommand('copy');
|
copy: function(text) {
|
||||||
if (success) {
|
if (Launcher) {
|
||||||
this.copied();
|
Launcher.setClipboardText(text);
|
||||||
|
var clipboardSeconds = AppSettingsModel.instance.get('clipboardSeconds');
|
||||||
|
if (clipboardSeconds > 0) {
|
||||||
|
setTimeout(function () {
|
||||||
|
if (Launcher.getClipboardText() === text) {
|
||||||
|
Launcher.clearClipboardText();
|
||||||
|
}
|
||||||
|
}, clipboardSeconds * 1000);
|
||||||
}
|
}
|
||||||
return success;
|
return {success: true, seconds: clipboardSeconds};
|
||||||
} catch (e) {
|
} else {
|
||||||
|
try {
|
||||||
|
if (document.execCommand('copy')) {
|
||||||
|
return {success: true};
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -37,22 +49,6 @@ var CopyPaste = {
|
||||||
'copy cut paste': function() { setTimeout(function() { hiddenInput.blur(); }, 0); },
|
'copy cut paste': function() { setTimeout(function() { hiddenInput.blur(); }, 0); },
|
||||||
blur: function() { hiddenInput.remove(); }
|
blur: function() { hiddenInput.remove(); }
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
copied: function() {
|
|
||||||
if (Launcher) {
|
|
||||||
var clipboardSeconds = AppSettingsModel.instance.get('clipboardSeconds');
|
|
||||||
if (clipboardSeconds > 0) {
|
|
||||||
setTimeout(function() {
|
|
||||||
setTimeout((function (prevText) {
|
|
||||||
if (Launcher.getClipboardText() === prevText) {
|
|
||||||
Launcher.clearClipboardText();
|
|
||||||
}
|
|
||||||
}).bind(null, Launcher.getClipboardText()), clipboardSeconds * 1000);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
return clipboardSeconds;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,6 @@ if (window.process && window.process.versions && window.process.versions.electro
|
||||||
openDevTools: function() {
|
openDevTools: function() {
|
||||||
this.req('remote').getCurrentWindow().openDevTools();
|
this.req('remote').getCurrentWindow().openDevTools();
|
||||||
},
|
},
|
||||||
getAppVersion: function() {
|
|
||||||
return this.remReq('app').getVersion();
|
|
||||||
},
|
|
||||||
getSaveFileName: function(defaultPath, cb) {
|
getSaveFileName: function(defaultPath, cb) {
|
||||||
if (defaultPath) {
|
if (defaultPath) {
|
||||||
var homePath = this.remReq('app').getPath('userDesktop');
|
var homePath = this.remReq('app').getPath('userDesktop');
|
||||||
|
@ -53,6 +50,16 @@ if (window.process && window.process.versions && window.process.versions.electro
|
||||||
deleteFile: function(path) {
|
deleteFile: function(path) {
|
||||||
this.req('fs').unlinkSync(path);
|
this.req('fs').unlinkSync(path);
|
||||||
},
|
},
|
||||||
|
statFile: function(path) {
|
||||||
|
return this.req('fs').statSync(path);
|
||||||
|
},
|
||||||
|
parsePath: function(fileName) {
|
||||||
|
var path = this.req('path');
|
||||||
|
return { path: fileName, dir: path.dirname(fileName), file: path.basename(fileName) };
|
||||||
|
},
|
||||||
|
createFsWatcher: function(path) {
|
||||||
|
return this.req('fs').watch(path, { persistent: false });
|
||||||
|
},
|
||||||
preventExit: function(e) {
|
preventExit: function(e) {
|
||||||
e.returnValue = false;
|
e.returnValue = false;
|
||||||
return false;
|
return false;
|
||||||
|
@ -76,6 +83,9 @@ if (window.process && window.process.versions && window.process.versions.electro
|
||||||
cancelRestart: function() {
|
cancelRestart: function() {
|
||||||
this.restartPending = false;
|
this.restartPending = false;
|
||||||
},
|
},
|
||||||
|
setClipboardText: function(text) {
|
||||||
|
return this.req('clipboard').writeText(text);
|
||||||
|
},
|
||||||
getClipboardText: function() {
|
getClipboardText: function() {
|
||||||
return this.req('clipboard').readText();
|
return this.req('clipboard').readText();
|
||||||
},
|
},
|
||||||
|
@ -87,6 +97,18 @@ if (window.process && window.process.versions && window.process.versions.electro
|
||||||
},
|
},
|
||||||
canMinimize: function() {
|
canMinimize: function() {
|
||||||
return process.platform === 'win32';
|
return process.platform === 'win32';
|
||||||
|
},
|
||||||
|
updaterEnabled: function() {
|
||||||
|
return this.req('remote').process.argv.indexOf('--disable-updater') === -1;
|
||||||
|
},
|
||||||
|
resolveProxy: function(url, callback) {
|
||||||
|
var window = this.remReq('app').getMainWindow();
|
||||||
|
var session = window.webContents.session;
|
||||||
|
session.resolveProxy(url, function(proxy) {
|
||||||
|
var match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
|
||||||
|
proxy = match && match[1] ? { host: match[1], port: +match[2] } : null;
|
||||||
|
callback(proxy);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Backbone.on('launcher-exit-request', function() {
|
Backbone.on('launcher-exit-request', function() {
|
||||||
|
|
|
@ -28,43 +28,58 @@ var Transport = {
|
||||||
logger.info('GET ' + config.url);
|
logger.info('GET ' + config.url);
|
||||||
var opts = Launcher.req('url').parse(config.url);
|
var opts = Launcher.req('url').parse(config.url);
|
||||||
opts.headers = { 'User-Agent': navigator.userAgent };
|
opts.headers = { 'User-Agent': navigator.userAgent };
|
||||||
Launcher.req(proto).get(opts, function(res) {
|
Launcher.resolveProxy(config.url, function(proxy) {
|
||||||
logger.info('Response from ' + config.url + ': ' + res.statusCode);
|
logger.info('Request to ' + config.url + ' ' + (proxy ? 'using proxy ' + proxy.host + ':' + proxy.port : 'without proxy'));
|
||||||
if (res.statusCode === 200) {
|
if (proxy) {
|
||||||
if (config.file) {
|
opts.headers.Host = opts.host;
|
||||||
var file = fs.createWriteStream(tmpFile);
|
opts.host = proxy.host;
|
||||||
res.pipe(file);
|
opts.port = proxy.port;
|
||||||
file.on('finish', function() {
|
opts.path = config.url;
|
||||||
file.close(function() { config.success(tmpFile); });
|
}
|
||||||
});
|
Launcher.req(proto).get(opts, function (res) {
|
||||||
file.on('error', function(err) { config.error(err); });
|
logger.info('Response from ' + config.url + ': ' + res.statusCode);
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
if (config.file) {
|
||||||
|
var file = fs.createWriteStream(tmpFile);
|
||||||
|
res.pipe(file);
|
||||||
|
file.on('finish', function () {
|
||||||
|
file.close(function () {
|
||||||
|
config.success(tmpFile);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
file.on('error', function (err) {
|
||||||
|
config.error(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var data = [];
|
||||||
|
res.on('data', function (chunk) {
|
||||||
|
data.push(chunk);
|
||||||
|
});
|
||||||
|
res.on('end', function () {
|
||||||
|
data = window.Buffer.concat(data);
|
||||||
|
if (config.utf8) {
|
||||||
|
data = data.toString('utf8');
|
||||||
|
}
|
||||||
|
config.success(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (res.headers.location && [301, 302].indexOf(res.statusCode) >= 0) {
|
||||||
|
if (config.noRedirect) {
|
||||||
|
return config.error('Too many redirects');
|
||||||
|
}
|
||||||
|
config.url = res.headers.location;
|
||||||
|
config.noRedirect = true;
|
||||||
|
Transport.httpGet(config);
|
||||||
} else {
|
} else {
|
||||||
var data = [];
|
config.error('HTTP status ' + res.statusCode);
|
||||||
res.on('data', function(chunk) { data.push(chunk); });
|
|
||||||
res.on('end', function() {
|
|
||||||
data = window.Buffer.concat(data);
|
|
||||||
if (config.utf8) {
|
|
||||||
data = data.toString('utf8');
|
|
||||||
}
|
|
||||||
config.success(data);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (res.headers.location && [301, 302].indexOf(res.statusCode) >= 0) {
|
}).on('error', function (e) {
|
||||||
if (config.noRedirect) {
|
logger.error('Cannot GET ' + config.url, e);
|
||||||
return config.error('Too many redirects');
|
if (tmpFile) {
|
||||||
|
fs.unlink(tmpFile);
|
||||||
}
|
}
|
||||||
config.url = res.headers.location;
|
config.error(e);
|
||||||
config.noRedirect = true;
|
});
|
||||||
Transport.httpGet(config);
|
|
||||||
} else {
|
|
||||||
config.error('HTTP status ' + res.statusCode);
|
|
||||||
}
|
|
||||||
}).on('error', function(e) {
|
|
||||||
logger.error('Cannot GET ' + config.url, e);
|
|
||||||
if (tmpFile) {
|
|
||||||
fs.unlink(tmpFile);
|
|
||||||
}
|
|
||||||
config.error(e);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,9 +18,10 @@ var Updater = {
|
||||||
UpdateCheckFiles: ['index.html', 'app.js'],
|
UpdateCheckFiles: ['index.html', 'app.js'],
|
||||||
nextCheckTimeout: null,
|
nextCheckTimeout: null,
|
||||||
updateCheckDate: new Date(0),
|
updateCheckDate: new Date(0),
|
||||||
|
enabled: Launcher && Launcher.updaterEnabled(),
|
||||||
|
|
||||||
getAutoUpdateType: function() {
|
getAutoUpdateType: function() {
|
||||||
if (!Launcher) {
|
if (!this.enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var autoUpdate = AppSettingsModel.instance.get('autoUpdate');
|
var autoUpdate = AppSettingsModel.instance.get('autoUpdate');
|
||||||
|
@ -65,10 +66,10 @@ var Updater = {
|
||||||
},
|
},
|
||||||
|
|
||||||
check: function(startedByUser) {
|
check: function(startedByUser) {
|
||||||
if (!Launcher || this.updateInProgress()) {
|
if (!startedByUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.checkManualDownload()) {
|
if (!this.enabled || this.updateInProgress()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UpdateModel.instance.set('status', 'checking');
|
UpdateModel.instance.set('status', 'checking');
|
||||||
|
@ -85,7 +86,7 @@ var Updater = {
|
||||||
}
|
}
|
||||||
logger.info('Checking for update...');
|
logger.info('Checking for update...');
|
||||||
Transport.httpGet({
|
Transport.httpGet({
|
||||||
url: Links.WebApp + 'manifest.appcache',
|
url: Links.Manifest,
|
||||||
utf8: true,
|
utf8: true,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
var dt = new Date();
|
var dt = new Date();
|
||||||
|
@ -111,6 +112,9 @@ var Updater = {
|
||||||
});
|
});
|
||||||
UpdateModel.instance.save();
|
UpdateModel.instance.save();
|
||||||
that.scheduleNextCheck();
|
that.scheduleNextCheck();
|
||||||
|
if (!that.canAutoUpdate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (prevLastVersion === UpdateModel.instance.get('lastVersion') &&
|
if (prevLastVersion === UpdateModel.instance.get('lastVersion') &&
|
||||||
UpdateModel.instance.get('updateStatus') === 'ready') {
|
UpdateModel.instance.get('updateStatus') === 'ready') {
|
||||||
logger.info('Waiting for the user to apply downloaded update');
|
logger.info('Waiting for the user to apply downloaded update');
|
||||||
|
@ -118,7 +122,7 @@ var Updater = {
|
||||||
}
|
}
|
||||||
if (!startedByUser && that.getAutoUpdateType() === 'install') {
|
if (!startedByUser && that.getAutoUpdateType() === 'install') {
|
||||||
that.update(startedByUser);
|
that.update(startedByUser);
|
||||||
} else if (UpdateModel.instance.get('lastVersion') !== RuntimeInfo.version) {
|
} else if (that.compareVersions(UpdateModel.instance.get('lastVersion'), RuntimeInfo.version) > 0) {
|
||||||
UpdateModel.instance.set('updateStatus', 'found');
|
UpdateModel.instance.set('updateStatus', 'found');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -135,16 +139,41 @@ var Updater = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
checkManualDownload: function() {
|
canAutoUpdate: function() {
|
||||||
if (+Launcher.getAppVersion().split('.')[1] <= 2) {
|
var minLauncherVersion = UpdateModel.instance.get('lastCheckUpdMin');
|
||||||
UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true });
|
if (minLauncherVersion) {
|
||||||
return true;
|
var cmp = this.compareVersions(Launcher.version, minLauncherVersion);
|
||||||
|
if (cmp < 0) {
|
||||||
|
UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
compareVersions: function(left, right) {
|
||||||
|
left = left.split('.');
|
||||||
|
right = right.split('.');
|
||||||
|
for (var num = 0; num < left.length; num++) {
|
||||||
|
var partLeft = left[num] | 0,
|
||||||
|
partRight = right[num] | 0;
|
||||||
|
if (partLeft < partRight) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (partLeft > partRight) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
update: function(startedByUser, successCallback) {
|
update: function(startedByUser, successCallback) {
|
||||||
var ver = UpdateModel.instance.get('lastVersion');
|
var ver = UpdateModel.instance.get('lastVersion');
|
||||||
if (!Launcher || ver === RuntimeInfo.version) {
|
if (!this.enabled) {
|
||||||
|
logger.info('Updater is disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.compareVersions(RuntimeInfo.version, ver) >= 0) {
|
||||||
logger.info('You are using the latest version');
|
logger.info('You are using the latest version');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@ var Links = {
|
||||||
License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt',
|
License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt',
|
||||||
UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
|
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',
|
ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes',
|
||||||
SelfHostedDropbox: 'https://github.com/antelle/keeweb#self-hosting'
|
SelfHostedDropbox: 'https://github.com/antelle/keeweb#self-hosting',
|
||||||
|
Manifest: 'https://antelle.github.io/keeweb/manifest.appcache'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Links;
|
module.exports = Links;
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
var Timeouts = {
|
var Timeouts = {
|
||||||
AutoSync: 30 * 1000 * 60,
|
AutoSync: 30 * 1000 * 60,
|
||||||
CopyTip: 1500,
|
CopyTip: 1500,
|
||||||
AutoHideHint: 3000
|
AutoHideHint: 3000,
|
||||||
|
FileChangeSync: 3000
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Timeouts;
|
module.exports = Timeouts;
|
||||||
|
|
|
@ -9,7 +9,11 @@ var isEnabled = FeatureDetector.isDesktop();
|
||||||
var Scrollable = {
|
var Scrollable = {
|
||||||
createScroll: function(opts) {
|
createScroll: function(opts) {
|
||||||
opts.$ = Backbone.$;
|
opts.$ = Backbone.$;
|
||||||
|
//opts.cssGuru = true;
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
|
if (this.scroll) {
|
||||||
|
this.removeScroll();
|
||||||
|
}
|
||||||
this.scroll = baron(opts);
|
this.scroll = baron(opts);
|
||||||
}
|
}
|
||||||
this.scroller = this.$el.find('.scroller');
|
this.scroller = this.$el.find('.scroller');
|
||||||
|
@ -17,6 +21,18 @@ var Scrollable = {
|
||||||
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
|
this.scrollerBarWrapper = this.$el.find('.scroller__bar-wrapper');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeScroll: function() {
|
||||||
|
if (this.scroll) {
|
||||||
|
this.scroll.dispose();
|
||||||
|
// TODO: remove once the bug in custom scrollbar is resolved
|
||||||
|
var ix = baron._instances.indexOf(this.scroll[0]);
|
||||||
|
if (ix >= 0) {
|
||||||
|
baron._instances.splice(ix, 1);
|
||||||
|
}
|
||||||
|
this.scroll = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
pageResized: function() {
|
pageResized: function() {
|
||||||
// TODO: check size on window resize
|
// TODO: check size on window resize
|
||||||
//if (this.checkSize && (!e || e.source === 'window')) {
|
//if (this.checkSize && (!e || e.source === 'window')) {
|
||||||
|
|
|
@ -42,18 +42,22 @@ _.extend(Backbone.View.prototype, {
|
||||||
},
|
},
|
||||||
|
|
||||||
renderTemplate: function(model, replace) {
|
renderTemplate: function(model, replace) {
|
||||||
if (replace) {
|
if (replace && replace.plain) {
|
||||||
this.$el.html('');
|
this.$el.html(this.template(model));
|
||||||
}
|
|
||||||
var el = $(this.template(model));
|
|
||||||
if (!this._elAppended || replace) {
|
|
||||||
this.$el.append(el);
|
|
||||||
this._elAppended = true;
|
|
||||||
} else {
|
} else {
|
||||||
this.$el.replaceWith(el);
|
if (replace) {
|
||||||
|
this.$el.html('');
|
||||||
|
}
|
||||||
|
var el = $(this.template(model));
|
||||||
|
if (!this._elAppended || replace) {
|
||||||
|
this.$el.append(el);
|
||||||
|
this._elAppended = true;
|
||||||
|
} else {
|
||||||
|
this.$el.replaceWith(el);
|
||||||
|
}
|
||||||
|
this.setElement(el);
|
||||||
}
|
}
|
||||||
this.setElement(el);
|
Tip.createTips(this.$el);
|
||||||
Tip.createTips(el);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_parentRemove: Backbone.View.prototype.remove,
|
_parentRemove: Backbone.View.prototype.remove,
|
||||||
|
|
|
@ -11,6 +11,7 @@ var Backbone = require('backbone'),
|
||||||
FileModel = require('./file-model'),
|
FileModel = require('./file-model'),
|
||||||
FileInfoModel = require('./file-info-model'),
|
FileInfoModel = require('./file-info-model'),
|
||||||
Storage = require('../storage'),
|
Storage = require('../storage'),
|
||||||
|
Timeouts = require('../const/timeouts'),
|
||||||
IdGenerator = require('../util/id-generator'),
|
IdGenerator = require('../util/id-generator'),
|
||||||
Logger = require('../util/logger');
|
Logger = require('../util/logger');
|
||||||
|
|
||||||
|
@ -108,7 +109,11 @@ var AppModel = Backbone.Model.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
closeAllFiles: function() {
|
closeAllFiles: function() {
|
||||||
this.files.each(function(file) { file.close(); });
|
var that = this;
|
||||||
|
this.files.each(function(file) {
|
||||||
|
file.close();
|
||||||
|
that.fileClosed(file);
|
||||||
|
});
|
||||||
this.files.reset();
|
this.files.reset();
|
||||||
this.menu.groupsSection.removeAllItems();
|
this.menu.groupsSection.removeAllItems();
|
||||||
this.menu.tagsSection.set('scrollable', false);
|
this.menu.tagsSection.set('scrollable', false);
|
||||||
|
@ -119,6 +124,8 @@ var AppModel = Backbone.Model.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
closeFile: function(file) {
|
closeFile: function(file) {
|
||||||
|
file.close();
|
||||||
|
this.fileClosed(file);
|
||||||
this.files.remove(file);
|
this.files.remove(file);
|
||||||
this.updateTags();
|
this.updateTags();
|
||||||
this.menu.groupsSection.removeByFile(file);
|
this.menu.groupsSection.removeByFile(file);
|
||||||
|
@ -352,13 +359,14 @@ var AppModel = Backbone.Model.extend({
|
||||||
}
|
}
|
||||||
var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid();
|
var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid();
|
||||||
file.set('cacheId', cacheId);
|
file.set('cacheId', cacheId);
|
||||||
if (updateCacheOnSuccess && params.storage !== 'file') {
|
if (updateCacheOnSuccess) {
|
||||||
logger.info('Save loaded file to cache');
|
logger.info('Save loaded file to cache');
|
||||||
Storage.cache.save(cacheId, params.fileData);
|
Storage.cache.save(cacheId, params.fileData);
|
||||||
}
|
}
|
||||||
var rev = params.rev || fileInfo && fileInfo.get('rev');
|
var rev = params.rev || fileInfo && fileInfo.get('rev');
|
||||||
that.addToLastOpenFiles(file, rev);
|
that.addToLastOpenFiles(file, rev);
|
||||||
that.addFile(file);
|
that.addFile(file);
|
||||||
|
that.fileOpened(file);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -381,6 +389,21 @@ var AppModel = Backbone.Model.extend({
|
||||||
this.fileInfos.save();
|
this.fileInfos.save();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fileOpened: function(file) {
|
||||||
|
var that = this;
|
||||||
|
if (file.get('storage') === 'file') {
|
||||||
|
Storage.file.watch(file.get('path'), _.debounce(function() {
|
||||||
|
that.syncFile(file);
|
||||||
|
}, Timeouts.FileChangeSync));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fileClosed: function(file) {
|
||||||
|
if (file.get('storage') === 'file') {
|
||||||
|
Storage.file.unwatch(file.get('path'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
removeFileInfo: function(id) {
|
removeFileInfo: function(id) {
|
||||||
Storage.cache.remove(id);
|
Storage.cache.remove(id);
|
||||||
this.fileInfos.remove(id);
|
this.fileInfos.remove(id);
|
||||||
|
@ -425,7 +448,7 @@ var AppModel = Backbone.Model.extend({
|
||||||
file.setSyncProgress();
|
file.setSyncProgress();
|
||||||
var complete = function(err, savedToCache) {
|
var complete = function(err, savedToCache) {
|
||||||
if (!err) { savedToCache = true; }
|
if (!err) { savedToCache = true; }
|
||||||
logger.info('Sync finished', err);
|
logger.info('Sync finished', err || 'no error');
|
||||||
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
|
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
|
||||||
file.set('cacheId', fileInfo.id);
|
file.set('cacheId', fileInfo.id);
|
||||||
fileInfo.set({
|
fileInfo.set({
|
||||||
|
@ -452,7 +475,7 @@ var AppModel = Backbone.Model.extend({
|
||||||
file.getData(function(data, err) {
|
file.getData(function(data, err) {
|
||||||
if (err) { return complete(err); }
|
if (err) { return complete(err); }
|
||||||
Storage.cache.save(fileInfo.id, data, function(err) {
|
Storage.cache.save(fileInfo.id, data, function(err) {
|
||||||
logger.info('Saved to cache', err);
|
logger.info('Saved to cache', err || 'no error');
|
||||||
complete(err);
|
complete(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -464,10 +487,10 @@ var AppModel = Backbone.Model.extend({
|
||||||
}
|
}
|
||||||
logger.info('Load from storage, attempt ' + loadLoops);
|
logger.info('Load from storage, attempt ' + loadLoops);
|
||||||
Storage[storage].load(path, function(err, data, stat) {
|
Storage[storage].load(path, function(err, data, stat) {
|
||||||
logger.info('Load from storage', stat, err);
|
logger.info('Load from storage', stat, err || 'no error');
|
||||||
if (err) { return complete(err); }
|
if (err) { return complete(err); }
|
||||||
file.mergeOrUpdate(data, options.remoteKey, function(err) {
|
file.mergeOrUpdate(data, options.remoteKey, function(err) {
|
||||||
logger.info('Merge complete', err);
|
logger.info('Merge complete', err || 'no error');
|
||||||
that.refresh();
|
that.refresh();
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.code === 'InvalidKey') {
|
if (err.code === 'InvalidKey') {
|
||||||
|
@ -484,7 +507,7 @@ var AppModel = Backbone.Model.extend({
|
||||||
if (file.get('modified')) {
|
if (file.get('modified')) {
|
||||||
logger.info('Updated sync date, saving modified file to cache and storage');
|
logger.info('Updated sync date, saving modified file to cache and storage');
|
||||||
saveToCacheAndStorage();
|
saveToCacheAndStorage();
|
||||||
} else if (file.get('dirty') && storage !== 'file') {
|
} else if (file.get('dirty')) {
|
||||||
logger.info('Saving not modified dirty file to cache');
|
logger.info('Saving not modified dirty file to cache');
|
||||||
Storage.cache.save(fileInfo.id, data, function (err) {
|
Storage.cache.save(fileInfo.id, data, function (err) {
|
||||||
if (err) { return complete(err); }
|
if (err) { return complete(err); }
|
||||||
|
@ -503,8 +526,8 @@ var AppModel = Backbone.Model.extend({
|
||||||
logger.info('Save to cache and storage');
|
logger.info('Save to cache and storage');
|
||||||
file.getData(function(data, err) {
|
file.getData(function(data, err) {
|
||||||
if (err) { return complete(err); }
|
if (err) { return complete(err); }
|
||||||
if (!file.get('dirty') || storage === 'file') {
|
if (!file.get('dirty')) {
|
||||||
logger.info('Save to storage, skip cache because not dirty or file storage');
|
logger.info('Save to storage, skip cache because not dirty');
|
||||||
saveToStorage(data);
|
saveToStorage(data);
|
||||||
} else {
|
} else {
|
||||||
logger.info('Saving to cache');
|
logger.info('Saving to cache');
|
||||||
|
@ -527,9 +550,6 @@ var AppModel = Backbone.Model.extend({
|
||||||
logger.info('Error saving data to storage');
|
logger.info('Error saving data to storage');
|
||||||
complete(err);
|
complete(err);
|
||||||
} else {
|
} else {
|
||||||
if (storage === 'file') {
|
|
||||||
Storage.cache.remove(fileInfo.id);
|
|
||||||
}
|
|
||||||
if (stat && stat.rev) {
|
if (stat && stat.rev) {
|
||||||
logger.info('Update rev in file info');
|
logger.info('Update rev in file info');
|
||||||
fileInfo.set('rev', stat.rev);
|
fileInfo.set('rev', stat.rev);
|
||||||
|
@ -540,55 +560,42 @@ var AppModel = Backbone.Model.extend({
|
||||||
}
|
}
|
||||||
}, fileInfo.get('rev'));
|
}, fileInfo.get('rev'));
|
||||||
};
|
};
|
||||||
if (options.reload) {
|
logger.info('Stat file');
|
||||||
logger.info('Saved to cache');
|
Storage[storage].stat(path, function (err, stat) {
|
||||||
loadFromStorageAndMerge();
|
if (err) {
|
||||||
} else if (storage === 'file') {
|
if (err.notFound) {
|
||||||
if (file.get('modified') || file.get('path') !== path) {
|
logger.info('File does not exist in storage, creating');
|
||||||
logger.info('Save modified file to storage');
|
saveToCacheAndStorage();
|
||||||
saveToCacheAndStorage();
|
} else if (file.get('dirty')) {
|
||||||
} else {
|
logger.info('Stat error, dirty, save to cache', err || 'no error');
|
||||||
logger.info('Skip not modified file');
|
file.getData(function (data) {
|
||||||
complete();
|
if (data) {
|
||||||
}
|
Storage.cache.save(fileInfo.id, data, function (e) {
|
||||||
} else {
|
if (!e) {
|
||||||
logger.info('Stat file');
|
file.set('dirty', false);
|
||||||
Storage[storage].stat(path, function (err, stat) {
|
}
|
||||||
if (err) {
|
logger.info('Saved to cache, exit with error', err || 'no error');
|
||||||
if (err.notFound) {
|
complete(err);
|
||||||
logger.info('File does not exist in storage, creating');
|
});
|
||||||
saveToCacheAndStorage();
|
}
|
||||||
} else if (file.get('dirty')) {
|
});
|
||||||
logger.info('Stat error, dirty, save to cache', err);
|
|
||||||
file.getData(function (data) {
|
|
||||||
if (data) {
|
|
||||||
Storage.cache.save(fileInfo.id, data, function (e) {
|
|
||||||
if (!e) {
|
|
||||||
file.set('dirty', false);
|
|
||||||
}
|
|
||||||
logger.info('Saved to cache, exit with error', err);
|
|
||||||
complete(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.info('Stat error, not dirty', err);
|
|
||||||
complete(err);
|
|
||||||
}
|
|
||||||
} else if (stat.rev === fileInfo.get('rev')) {
|
|
||||||
if (file.get('modified')) {
|
|
||||||
logger.info('Stat found same version, modified, saving to cache and storage');
|
|
||||||
saveToCacheAndStorage();
|
|
||||||
} else {
|
|
||||||
logger.info('Stat found same version, not modified');
|
|
||||||
complete();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.info('Found new version, loading from storage');
|
logger.info('Stat error, not dirty', err || 'no error');
|
||||||
loadFromStorageAndMerge();
|
complete(err);
|
||||||
}
|
}
|
||||||
});
|
} else if (stat.rev === fileInfo.get('rev')) {
|
||||||
}
|
if (file.get('modified')) {
|
||||||
|
logger.info('Stat found same version, modified, saving to cache and storage');
|
||||||
|
saveToCacheAndStorage();
|
||||||
|
} else {
|
||||||
|
logger.info('Stat found same version, not modified');
|
||||||
|
complete();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info('Found new version, loading from storage');
|
||||||
|
loadFromStorageAndMerge();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -183,6 +183,11 @@ var GroupModel = MenuItemModel.extend({
|
||||||
}
|
}
|
||||||
this.file.setModified();
|
this.file.setModified();
|
||||||
if (object instanceof GroupModel) {
|
if (object instanceof GroupModel) {
|
||||||
|
for (var parent = this; parent; parent = parent.parentGroup) {
|
||||||
|
if (object === parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (this.group.groups.indexOf(object.group) >= 0) {
|
if (this.group.groups.indexOf(object.group) >= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ var Launcher = require('../comp/launcher'),
|
||||||
|
|
||||||
var logger = new Logger('storage-file');
|
var logger = new Logger('storage-file');
|
||||||
|
|
||||||
|
var fileWatchers = {};
|
||||||
|
|
||||||
var StorageFile = {
|
var StorageFile = {
|
||||||
name: 'file',
|
name: 'file',
|
||||||
enabled: !!Launcher,
|
enabled: !!Launcher,
|
||||||
|
@ -14,25 +16,95 @@ var StorageFile = {
|
||||||
var ts = logger.ts();
|
var ts = logger.ts();
|
||||||
try {
|
try {
|
||||||
var data = Launcher.readFile(path);
|
var data = Launcher.readFile(path);
|
||||||
logger.debug('Loaded', path, logger.ts(ts));
|
var rev = Launcher.statFile(path).mtime.getTime().toString();
|
||||||
if (callback) { callback(null, data.buffer); }
|
logger.debug('Loaded', path, rev, logger.ts(ts));
|
||||||
|
if (callback) { callback(null, data.buffer, { rev: rev }); }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error reading local file', path, e);
|
logger.error('Error reading local file', path, e);
|
||||||
if (callback) { callback(e, null); }
|
if (callback) { callback(e, null); }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
save: function(path, data, callback) {
|
stat: function(path, callback) {
|
||||||
logger.debug('Save', path);
|
logger.debug('Stat', path);
|
||||||
var ts = logger.ts();
|
var ts = logger.ts();
|
||||||
try {
|
try {
|
||||||
|
var stat = Launcher.statFile(path);
|
||||||
|
logger.debug('Stat done', path, logger.ts(ts));
|
||||||
|
if (callback) { callback(null, { rev: stat.mtime.getTime().toString() }); }
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error stat local file', path, e);
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
e.notFound = true;
|
||||||
|
}
|
||||||
|
if (callback) { callback(e, null); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function(path, data, callback, rev) {
|
||||||
|
logger.debug('Save', path, rev);
|
||||||
|
var ts = logger.ts();
|
||||||
|
try {
|
||||||
|
if (rev) {
|
||||||
|
try {
|
||||||
|
var stat = Launcher.statFile(path);
|
||||||
|
var fileRev = stat.mtime.getTime().toString();
|
||||||
|
if (fileRev !== rev) {
|
||||||
|
logger.debug('Save mtime differs', rev, fileRev);
|
||||||
|
if (callback) { callback({ revConflict: true }, { rev: fileRev }); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// file doesn't exist or we cannot stat it: don't care and overwrite
|
||||||
|
}
|
||||||
|
}
|
||||||
Launcher.writeFile(path, data);
|
Launcher.writeFile(path, data);
|
||||||
|
var newRev = Launcher.statFile(path).mtime.getTime().toString();
|
||||||
logger.debug('Saved', path, logger.ts(ts));
|
logger.debug('Saved', path, logger.ts(ts));
|
||||||
if (callback) { callback(); }
|
if (callback) { callback(undefined, { rev: newRev }); }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error writing local file', path, e);
|
logger.error('Error writing local file', path, e);
|
||||||
if (callback) { callback(e); }
|
if (callback) { callback(e); }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: function(path, callback) {
|
||||||
|
var names = Launcher.parsePath(path);
|
||||||
|
if (!fileWatchers[names.dir]) {
|
||||||
|
logger.debug('Watch dir', names.dir);
|
||||||
|
var fsWatcher = Launcher.createFsWatcher(names.dir);
|
||||||
|
fsWatcher.on('change', this.fsWatcherChange.bind(this, names.dir));
|
||||||
|
fileWatchers[names.dir] = { fsWatcher: fsWatcher, callbacks: [] };
|
||||||
|
}
|
||||||
|
fileWatchers[names.dir].callbacks.push({ file: names.file, callback: callback });
|
||||||
|
},
|
||||||
|
|
||||||
|
unwatch: function(path) {
|
||||||
|
var names = Launcher.parsePath(path);
|
||||||
|
var watcher = fileWatchers[names.dir];
|
||||||
|
if (watcher) {
|
||||||
|
var ix = watcher.callbacks.findIndex(function(cb) { return cb.file === names.file; });
|
||||||
|
if (ix >= 0) {
|
||||||
|
watcher.callbacks.splice(ix, 1);
|
||||||
|
}
|
||||||
|
if (!watcher.callbacks.length) {
|
||||||
|
logger.debug('Stop watch dir', names.dir);
|
||||||
|
watcher.fsWatcher.close();
|
||||||
|
delete fileWatchers[names.dir];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fsWatcherChange: function(dirname, evt, fileName) {
|
||||||
|
var watcher = fileWatchers[dirname];
|
||||||
|
if (watcher) {
|
||||||
|
watcher.callbacks.forEach(function(cb) {
|
||||||
|
if (cb.file === fileName && typeof cb.callback === 'function') {
|
||||||
|
logger.debug('File changed', dirname, evt, fileName);
|
||||||
|
cb.callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,10 @@ var Locale = {
|
||||||
|
|
||||||
footerOpen: 'Open / New',
|
footerOpen: 'Open / New',
|
||||||
footerSyncError: 'Sync error',
|
footerSyncError: 'Sync error',
|
||||||
|
footerTitleHelp: 'Help',
|
||||||
|
footerTitleSettings: 'Settings',
|
||||||
|
footerTitleGen: 'Generate',
|
||||||
|
footerTitleLock: 'Lock',
|
||||||
|
|
||||||
genLen: 'Length',
|
genLen: 'Length',
|
||||||
grpTitle: 'Group',
|
grpTitle: 'Group',
|
||||||
|
@ -147,7 +151,7 @@ var Locale = {
|
||||||
appSecWarn: 'Not Secure!',
|
appSecWarn: 'Not Secure!',
|
||||||
appSecWarnBody1: 'You have loaded this app with insecure connection. ' +
|
appSecWarnBody1: 'You have loaded this app with insecure connection. ' +
|
||||||
'Someone may be watching you and stealing your passwords. ' +
|
'Someone may be watching you and stealing your passwords. ' +
|
||||||
'We strongly advice you to stop, unless you clearly understand what you\'re doing.',
|
'We strongly advise you to stop, unless you clearly understand what you\'re doing.',
|
||||||
appSecWarnBody2: 'Yes, your database is encrypted but no one can guarantee that the app has not been modified on the way to you.',
|
appSecWarnBody2: 'Yes, your database is encrypted but no one can guarantee that the app has not been modified on the way to you.',
|
||||||
appSecWarnBtn: 'I understand the risks, continue',
|
appSecWarnBtn: 'I understand the risks, continue',
|
||||||
appUnsavedWarn: 'Unsaved changes!',
|
appUnsavedWarn: 'Unsaved changes!',
|
||||||
|
@ -168,7 +172,7 @@ var Locale = {
|
||||||
setGenUpdate: 'Update',
|
setGenUpdate: 'Update',
|
||||||
setGenNewVersion: 'New app version was released and downloaded',
|
setGenNewVersion: 'New app version was released and downloaded',
|
||||||
setGenReleaseNotes: 'View release notes',
|
setGenReleaseNotes: 'View release notes',
|
||||||
setGenReloadTpUpdate: 'Reload to update',
|
setGenReloadToUpdate: 'Reload to update',
|
||||||
setGenUpdateManual: 'New version has been released. It will check for updates and install them automatically ' +
|
setGenUpdateManual: 'New version has been released. It will check for updates and install them automatically ' +
|
||||||
'but auto-upgrading from your version is impossible.',
|
'but auto-upgrading from your version is impossible.',
|
||||||
setGenDownloadUpdate: 'Download update',
|
setGenDownloadUpdate: 'Download update',
|
||||||
|
@ -305,7 +309,7 @@ var Locale = {
|
||||||
dropboxFullBody: 'Your Dropbox is full, there\'s no space left anymore.',
|
dropboxFullBody: 'Your Dropbox is full, there\'s no space left anymore.',
|
||||||
dropboxRateLimitedBody: 'Too many requests to Dropbox have been made by this app. Please, try again later.',
|
dropboxRateLimitedBody: 'Too many requests to Dropbox have been made by this app. Please, try again later.',
|
||||||
dropboxNetError: 'Dropbox Sync Network Error',
|
dropboxNetError: 'Dropbox Sync Network Error',
|
||||||
dropboxNetErrorBody: 'Network error occured during Dropbox sync. Please, check your connection and try again.',
|
dropboxNetErrorBody: 'Network error occurred during Dropbox sync. Please, check your connection and try again.',
|
||||||
dropboxErrorBody: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ',
|
dropboxErrorBody: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ',
|
||||||
dropboxErrorRepeatBody: 'Something went wrong during Dropbox sync. Please, try again later. Error: ',
|
dropboxErrorRepeatBody: 'Something went wrong during Dropbox sync. Please, try again later. Error: ',
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var FeatureDetector = require('./feature-detector');
|
var Backbone = require('backbone'),
|
||||||
|
FeatureDetector = require('./feature-detector');
|
||||||
|
|
||||||
var Tip = function(el, config) {
|
var Tip = function(el, config) {
|
||||||
this.el = el;
|
this.el = el;
|
||||||
|
@ -10,6 +11,7 @@ var Tip = function(el, config) {
|
||||||
this.tipEl = null;
|
this.tipEl = null;
|
||||||
this.showTimeout = null;
|
this.showTimeout = null;
|
||||||
this.hideTimeout = null;
|
this.hideTimeout = null;
|
||||||
|
this.hide = this.hide.bind(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
Tip.enabled = FeatureDetector.isDesktop();
|
Tip.enabled = FeatureDetector.isDesktop();
|
||||||
|
@ -27,6 +29,7 @@ Tip.prototype.show = function() {
|
||||||
if (!Tip.enabled) {
|
if (!Tip.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Backbone.on('page-geometry', this.hide);
|
||||||
if (this.tipEl) {
|
if (this.tipEl) {
|
||||||
this.tipEl.remove();
|
this.tipEl.remove();
|
||||||
if (this.hideTimeout) {
|
if (this.hideTimeout) {
|
||||||
|
@ -44,11 +47,16 @@ Tip.prototype.show = function() {
|
||||||
}
|
}
|
||||||
var top, left;
|
var top, left;
|
||||||
var offset = 10;
|
var offset = 10;
|
||||||
|
var sideOffset = 10;
|
||||||
switch (placement) {
|
switch (placement) {
|
||||||
case 'top':
|
case 'top':
|
||||||
top = rect.top - tipRect.height - offset;
|
top = rect.top - tipRect.height - offset;
|
||||||
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
||||||
break;
|
break;
|
||||||
|
case 'top-left':
|
||||||
|
top = rect.top - tipRect.height - offset;
|
||||||
|
left = rect.left + rect.width / 2 - tipRect.width + sideOffset;
|
||||||
|
break;
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
top = rect.bottom + offset;
|
top = rect.bottom + offset;
|
||||||
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
||||||
|
@ -70,6 +78,7 @@ Tip.prototype.hide = function() {
|
||||||
this.tipEl.remove();
|
this.tipEl.remove();
|
||||||
this.tipEl = null;
|
this.tipEl = null;
|
||||||
}
|
}
|
||||||
|
Backbone.off('page-geometry', this.hide);
|
||||||
};
|
};
|
||||||
|
|
||||||
Tip.prototype.mouseenter = function() {
|
Tip.prototype.mouseenter = function() {
|
||||||
|
@ -129,11 +138,8 @@ Tip.createTips = function(container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
container.find('[title]').each(function(ix, el) {
|
container.find('[title]').each(function(ix, el) {
|
||||||
if (!el._tip) {
|
var tip = new Tip($(el));
|
||||||
var tip = new Tip($(el));
|
tip.init();
|
||||||
tip.init();
|
|
||||||
el._tip = tip;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -408,10 +408,13 @@ var AppView = Backbone.View.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
closeAllFilesAndShowFirst: function() {
|
closeAllFilesAndShowFirst: function() {
|
||||||
var firstFile = this.model.files.find(function(file) { return !file.get('demo') && !file.get('created'); });
|
var fileToShow = this.model.files.find(function(file) { return !file.get('demo') && !file.get('created'); });
|
||||||
this.model.closeAllFiles();
|
this.model.closeAllFiles();
|
||||||
if (firstFile) {
|
if (!fileToShow) {
|
||||||
var fileInfo = this.model.fileInfos.getMatch(firstFile.get('storage'), firstFile.get('name'), firstFile.get('path'));
|
fileToShow = this.model.fileInfos.getLast();
|
||||||
|
}
|
||||||
|
if (fileToShow) {
|
||||||
|
var fileInfo = this.model.fileInfos.getMatch(fileToShow.get('storage'), fileToShow.get('name'), fileToShow.get('path'));
|
||||||
if (fileInfo) {
|
if (fileInfo) {
|
||||||
this.views.open.showOpenFileInfo(fileInfo);
|
this.views.open.showOpenFileInfo(fileInfo);
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ var DetailsView = Backbone.View.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
|
this.removeScroll();
|
||||||
this.removeFieldViews();
|
this.removeFieldViews();
|
||||||
if (this.views.sub) {
|
if (this.views.sub) {
|
||||||
this.views.sub.remove();
|
this.views.sub.remove();
|
||||||
|
@ -272,10 +273,13 @@ var DetailsView = Backbone.View.extend({
|
||||||
if (!window.getSelection().toString()) {
|
if (!window.getSelection().toString()) {
|
||||||
var pw = this.model.password;
|
var pw = this.model.password;
|
||||||
var password = pw.isProtected ? pw.getText() : pw;
|
var password = pw.isProtected ? pw.getText() : pw;
|
||||||
CopyPaste.createHiddenInput(password);
|
if (!CopyPaste.simpleCopy) {
|
||||||
var clipboardTime = CopyPaste.copied();
|
CopyPaste.createHiddenInput(password);
|
||||||
if (!this.passCopyTip) {
|
}
|
||||||
|
var copyRes = CopyPaste.copy(password);
|
||||||
|
if (copyRes && !this.passCopyTip) {
|
||||||
var passLabel = this.passEditView.labelEl;
|
var passLabel = this.passEditView.labelEl;
|
||||||
|
var clipboardTime = copyRes.seconds;
|
||||||
var msg = clipboardTime ? Locale.detPassCopiedTime.replace('{}', clipboardTime)
|
var msg = clipboardTime ? Locale.detPassCopiedTime.replace('{}', clipboardTime)
|
||||||
: Locale.detPassCopied;
|
: Locale.detPassCopied;
|
||||||
var tip = new Tip(passLabel, { title: msg, placement: 'right', fast: true });
|
var tip = new Tip(passLabel, { title: msg, placement: 'right', fast: true });
|
||||||
|
|
|
@ -41,14 +41,17 @@ var FieldView = Backbone.View.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CopyPaste.createHiddenInput(textValue, box);
|
CopyPaste.createHiddenInput(textValue, box);
|
||||||
//CopyPaste.tryCopy(); // maybe Apple will ever support this?
|
//CopyPaste.copy(); // maybe Apple will ever support this?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (field) {
|
if (field) {
|
||||||
var value = this.value || '';
|
var value = this.value || '';
|
||||||
if (value && value.isProtected) {
|
if (value && value.isProtected) {
|
||||||
CopyPaste.createHiddenInput(value.getText());
|
var text = value.getText();
|
||||||
CopyPaste.tryCopy();
|
if (!CopyPaste.simpleCopy) {
|
||||||
|
CopyPaste.createHiddenInput(text);
|
||||||
|
}
|
||||||
|
CopyPaste.copy(text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +60,7 @@ var FieldView = Backbone.View.extend({
|
||||||
range.selectNodeContents(this.valueEl[0]);
|
range.selectNodeContents(this.valueEl[0]);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
if (CopyPaste.tryCopy()) {
|
if (CopyPaste.copy(this.valueEl.text())) {
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,6 @@ var Backbone = require('backbone'),
|
||||||
Keys = require('../const/keys'),
|
Keys = require('../const/keys'),
|
||||||
KeyHandler = require('../comp/key-handler'),
|
KeyHandler = require('../comp/key-handler'),
|
||||||
GeneratorView = require('./generator-view'),
|
GeneratorView = require('./generator-view'),
|
||||||
Tip = require('../util/tip'),
|
|
||||||
UpdateModel = require('../models/update-model');
|
UpdateModel = require('../models/update-model');
|
||||||
|
|
||||||
var FooterView = Backbone.View.extend({
|
var FooterView = Backbone.View.extend({
|
||||||
|
@ -33,11 +32,10 @@ var FooterView = Backbone.View.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
this.$el.html(this.template({
|
this.renderTemplate({
|
||||||
files: this.model.files,
|
files: this.model.files,
|
||||||
updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
|
updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
|
||||||
}));
|
}, { plain: true });
|
||||||
Tip.createTips(this.$el);
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ var GeneratorView = Backbone.View.extend({
|
||||||
range.selectNodeContents(this.resultEl[0]);
|
range.selectNodeContents(this.resultEl[0]);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
CopyPaste.tryCopy();
|
CopyPaste.copy(this.password);
|
||||||
this.trigger('result', this.password);
|
this.trigger('result', this.password);
|
||||||
this.remove();
|
this.remove();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
var Backbone = require('backbone'),
|
var Backbone = require('backbone'),
|
||||||
Scrollable = require('../mixins/scrollable'),
|
Scrollable = require('../mixins/scrollable'),
|
||||||
IconSelectView = require('./icon-select-view'),
|
IconSelectView = require('./icon-select-view');
|
||||||
Tip = require('../util/tip');
|
|
||||||
|
|
||||||
var GrpView = Backbone.View.extend({
|
var GrpView = Backbone.View.extend({
|
||||||
template: require('templates/grp.hbs'),
|
template: require('templates/grp.hbs'),
|
||||||
|
@ -23,14 +22,13 @@ var GrpView = Backbone.View.extend({
|
||||||
render: function() {
|
render: function() {
|
||||||
this.removeSubView();
|
this.removeSubView();
|
||||||
if (this.model) {
|
if (this.model) {
|
||||||
this.$el.html(this.template({
|
this.renderTemplate({
|
||||||
title: this.model.get('title'),
|
title: this.model.get('title'),
|
||||||
icon: this.model.get('icon') || 'folder',
|
icon: this.model.get('icon') || 'folder',
|
||||||
customIcon: this.model.get('customIcon'),
|
customIcon: this.model.get('customIcon'),
|
||||||
enableSearching: this.model.get('enableSearching') !== false,
|
enableSearching: this.model.get('enableSearching') !== false,
|
||||||
readonly: this.model.get('top')
|
readonly: this.model.get('top')
|
||||||
}));
|
}, { plain: true });
|
||||||
Tip.createTips(this.$el);
|
|
||||||
if (!this.model.get('title')) {
|
if (!this.model.get('title')) {
|
||||||
this.$el.find('#grp__field-title').focus();
|
this.$el.find('#grp__field-title').focus();
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ var SettingsGeneralView = Backbone.View.extend({
|
||||||
idleMinutes: AppSettingsModel.instance.get('idleMinutes'),
|
idleMinutes: AppSettingsModel.instance.get('idleMinutes'),
|
||||||
minimizeOnClose: AppSettingsModel.instance.get('minimizeOnClose'),
|
minimizeOnClose: AppSettingsModel.instance.get('minimizeOnClose'),
|
||||||
devTools: Launcher && Launcher.devTools,
|
devTools: Launcher && Launcher.devTools,
|
||||||
canAutoUpdate: !!Launcher,
|
canAutoUpdate: Updater.enabled,
|
||||||
canMinimize: Launcher && Launcher.canMinimize(),
|
canMinimize: Launcher && Launcher.canMinimize(),
|
||||||
lockOnMinimize: Launcher && AppSettingsModel.instance.get('lockOnMinimize'),
|
lockOnMinimize: Launcher && AppSettingsModel.instance.get('lockOnMinimize'),
|
||||||
tableView: AppSettingsModel.instance.get('tableView'),
|
tableView: AppSettingsModel.instance.get('tableView'),
|
||||||
|
@ -66,7 +66,7 @@ var SettingsGeneralView = Backbone.View.extend({
|
||||||
updateInProgress: Updater.updateInProgress(),
|
updateInProgress: Updater.updateInProgress(),
|
||||||
updateInfo: this.getUpdateInfo(),
|
updateInfo: this.getUpdateInfo(),
|
||||||
updateWaitingReload: updateReady && !Launcher,
|
updateWaitingReload: updateReady && !Launcher,
|
||||||
showUpdateBlock: Launcher && !updateManual,
|
showUpdateBlock: Updater.enabled && !updateManual,
|
||||||
updateReady: updateReady,
|
updateReady: updateReady,
|
||||||
updateFound: updateFound,
|
updateFound: updateFound,
|
||||||
updateManual: updateManual,
|
updateManual: updateManual,
|
||||||
|
@ -91,7 +91,8 @@ var SettingsGeneralView = Backbone.View.extend({
|
||||||
return errMsg;
|
return errMsg;
|
||||||
case 'ok':
|
case 'ok':
|
||||||
var msg = Locale.setGenCheckedAt + ' ' + Format.dtStr(UpdateModel.instance.get('lastCheckDate')) + ': ';
|
var msg = Locale.setGenCheckedAt + ' ' + Format.dtStr(UpdateModel.instance.get('lastCheckDate')) + ': ';
|
||||||
if (RuntimeInfo.version === UpdateModel.instance.get('lastVersion')) {
|
var cmp = Updater.compareVersions(RuntimeInfo.version, UpdateModel.instance.get('lastVersion'));
|
||||||
|
if (cmp >= 0) {
|
||||||
msg += Locale.setGenLatestVer;
|
msg += Locale.setGenLatestVer;
|
||||||
} else {
|
} else {
|
||||||
msg += Locale.setGenNewVer.replace('{}', UpdateModel.instance.get('lastVersion')) + ' ' +
|
msg += Locale.setGenNewVer.replace('{}', UpdateModel.instance.get('lastVersion')) + ' ' +
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: $base-padding-v;
|
padding-bottom: $base-padding-v;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
line-height: $mobile-back-button-height;
|
||||||
|
height: $mobile-back-button-height;
|
||||||
>i { margin-right: $base-padding-h; }
|
>i { margin-right: $base-padding-h; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,10 +132,6 @@
|
||||||
@include flex-wrap(wrap);
|
@include flex-wrap(wrap);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
|
|
||||||
@include scrollbar-full-width-hack();
|
|
||||||
@-moz-document url-prefix() { @include scrollbar-padding-hack(); }
|
|
||||||
@at-root { _:-ms-lang(x), .details__body>.scroller { @include scrollbar-padding-hack(); } }
|
|
||||||
@media screen and (-webkit-min-device-pixel-ratio:0) { width: 100% !important; }
|
@media screen and (-webkit-min-device-pixel-ratio:0) { width: 100% !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +195,7 @@
|
||||||
line-height: $details-field-line-height;
|
line-height: $details-field-line-height;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 20px;
|
||||||
.details__field--editable & {
|
.details__field--editable & {
|
||||||
border-radius: $base-border-radius;
|
border-radius: $base-border-radius;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -367,6 +366,7 @@
|
||||||
@include display(flex);
|
@include display(flex);
|
||||||
@include align-items(stretch);
|
@include align-items(stretch);
|
||||||
@include flex-direction(row);
|
@include flex-direction(row);
|
||||||
|
@include flex-shrink(0);
|
||||||
@include justify-content(flex-start);
|
@include justify-content(flex-start);
|
||||||
margin-top: $base-padding-v;
|
margin-top: $base-padding-v;
|
||||||
|
|
||||||
|
|
|
@ -21,11 +21,6 @@
|
||||||
@include flex(1);
|
@include flex(1);
|
||||||
@include align-self(stretch);
|
@include align-self(stretch);
|
||||||
position: relative;
|
position: relative;
|
||||||
.app__list-wrap--table & {
|
|
||||||
@include scrollbar-full-width-hack();
|
|
||||||
@-moz-document url-prefix() { @include scrollbar-padding-hack(); }
|
|
||||||
@at-root { _:-ms-lang(x), .app__list-wrap--table .list__items>.scroller { @include scrollbar-padding-hack(); } }
|
|
||||||
}
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
|
@ -124,7 +119,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-icon {
|
&-icon {
|
||||||
width: 14px;
|
width: 16px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
&--custom {
|
&--custom {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
@include display(flex);
|
@include display(flex);
|
||||||
@include align-items(stretch);
|
@include align-items(stretch);
|
||||||
@include flex-direction(row);
|
@include flex-direction(row);
|
||||||
|
@include flex-shrink(0);
|
||||||
.open--drag & { display: none; }
|
.open--drag & { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
@include flex-direction(row);
|
@include flex-direction(row);
|
||||||
@include justify-content(flex-start);
|
@include justify-content(flex-start);
|
||||||
@include align-items(stretch);
|
@include align-items(stretch);
|
||||||
|
@include flex-shrink(0);
|
||||||
margin-bottom: $base-padding-v;
|
margin-bottom: $base-padding-v;
|
||||||
}
|
}
|
||||||
&-enter-btn, &-opening-icon {
|
&-enter-btn, &-opening-icon {
|
||||||
|
@ -126,6 +128,7 @@
|
||||||
@include flex-direction(row);
|
@include flex-direction(row);
|
||||||
@include justify-content(flex-start);
|
@include justify-content(flex-start);
|
||||||
@include align-items(baseline);
|
@include align-items(baseline);
|
||||||
|
@include flex-shrink(0);
|
||||||
.open:not(.open--opening) & {
|
.open:not(.open--opening) & {
|
||||||
@include area-selectable;
|
@include area-selectable;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,6 @@
|
||||||
|
|
||||||
>.scroller {
|
>.scroller {
|
||||||
@include flex(1 0 0);
|
@include flex(1 0 0);
|
||||||
@include scrollbar-full-width-hack();
|
|
||||||
@-moz-document url-prefix() { @include scrollbar-padding-hack(); }
|
|
||||||
@at-root { _:-ms-lang(x), .settings>.scroller { @include scrollbar-padding-hack(); } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2,h3 {
|
h2,h3 {
|
||||||
|
@ -38,6 +35,8 @@
|
||||||
&-pre, &-post { display: none; }
|
&-pre, &-post { display: none; }
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@include mobile {
|
@include mobile {
|
||||||
|
line-height: $mobile-back-button-height;
|
||||||
|
height: $mobile-back-button-height;
|
||||||
padding-bottom: $base-padding-v;
|
padding-bottom: $base-padding-v;
|
||||||
>i { margin-right: $base-padding-h; }
|
>i { margin-right: $base-padding-h; }
|
||||||
&-pre { display: inline; }
|
&-pre { display: inline; }
|
||||||
|
|
|
@ -18,6 +18,7 @@ $small-spacing: $base-spacing / 2;
|
||||||
$base-z-index: 0;
|
$base-z-index: 0;
|
||||||
$base-padding-v: .4em;
|
$base-padding-v: .4em;
|
||||||
$base-padding-h: .8em;
|
$base-padding-h: .8em;
|
||||||
|
$mobile-back-button-height: 3em;
|
||||||
$base-padding: $base-padding-v $base-padding-h;
|
$base-padding: $base-padding-v $base-padding-h;
|
||||||
$medium-padding: .8em 1em;
|
$medium-padding: .8em 1em;
|
||||||
$base-padding-px: 4px 8px;
|
$base-padding-px: 4px 8px;
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
&.icon-select {
|
&.icon-select {
|
||||||
@include display(flex);
|
|
||||||
@include align-items(stretch);
|
|
||||||
@include flex-direction(column);
|
|
||||||
@include justify-content(flex-start);
|
|
||||||
&__items {
|
&__items {
|
||||||
@include flex(1 1 auto);
|
|
||||||
@include display(flex);
|
@include display(flex);
|
||||||
@include align-items(flex-start);
|
@include align-items(flex-start);
|
||||||
@include flex-direction(row);
|
@include flex-direction(row);
|
||||||
|
|
|
@ -1,24 +1,6 @@
|
||||||
@mixin scrollbar-padding-hack {
|
|
||||||
// workaround for bugs in custom scrollbar component (baron)
|
|
||||||
// https://github.com/Diokuz/baron/issues/91
|
|
||||||
// https://github.com/Diokuz/baron/issues/93
|
|
||||||
margin-right: -30px !important;
|
|
||||||
padding-right: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin scrollbar-full-width-hack {
|
|
||||||
// workaround for bugs in custom scrollbar component (baron)
|
|
||||||
width: auto !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
max-width: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroller {
|
.scroller {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@-moz-document url-prefix() {
|
|
||||||
@include scrollbar-padding-hack();
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
|
|
||||||
$arrow-size-small: 10px 8px;
|
$arrow-size-small: 10px 8px;
|
||||||
$arrow-size-large: 12px 9px;
|
$arrow-size-large: 12px 9px;
|
||||||
|
$arrow-offset-small: 10px;
|
||||||
|
$arrow-offset-large: 11px;
|
||||||
|
|
||||||
&.tip--bottom:after {
|
&.tip--bottom:after {
|
||||||
@include position(absolute, - nth($arrow-size-small, 2) null null 50%);
|
@include position(absolute, - nth($arrow-size-small, 2) null null 50%);
|
||||||
|
@ -38,6 +40,11 @@
|
||||||
@include transform(translate(-50%, 0));
|
@include transform(translate(-50%, 0));
|
||||||
@include th { @include triangle($arrow-size-small, background-color(), down); }
|
@include th { @include triangle($arrow-size-small, background-color(), down); }
|
||||||
}
|
}
|
||||||
|
&.tip--top-left:after {
|
||||||
|
@include position(absolute, 100% null null calc(100% - #{$arrow-offset-small}));
|
||||||
|
@include transform(translate(-50%, 0));
|
||||||
|
@include th { @include triangle($arrow-size-small, background-color(), down); }
|
||||||
|
}
|
||||||
&.tip--left:after {
|
&.tip--left:after {
|
||||||
@include position(absolute, 50% null null 100%);
|
@include position(absolute, 50% null null 100%);
|
||||||
@include transform(translate(0, -50%));
|
@include transform(translate(0, -50%));
|
||||||
|
@ -59,6 +66,11 @@
|
||||||
@include transform(translate(-50%, 0));
|
@include transform(translate(-50%, 0));
|
||||||
@include th { @include triangle($arrow-size-large, light-border-color(), down); }
|
@include th { @include triangle($arrow-size-large, light-border-color(), down); }
|
||||||
}
|
}
|
||||||
|
&.tip--top-left:before {
|
||||||
|
@include position(absolute, 100% null null calc(100% - #{$arrow-offset-small}));
|
||||||
|
@include transform(translate(-50%, 0));
|
||||||
|
@include th { @include triangle($arrow-size-large, light-border-color(), down); }
|
||||||
|
}
|
||||||
&.tip--left:before {
|
&.tip--left:before {
|
||||||
@include position(absolute, 50% null null 100%);
|
@include position(absolute, 50% null null 100%);
|
||||||
@include transform(translate(0, -50%));
|
@include transform(translate(0, -50%));
|
||||||
|
|
|
@ -13,14 +13,14 @@
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<div class="footer__db footer__db--dimmed footer__db--expanded footer__db-open"><i class="fa fa-plus"></i> {{res 'footerOpen'}}</div>
|
<div class="footer__db footer__db--dimmed footer__db--expanded footer__db-open"><i class="fa fa-plus"></i> {{res 'footerOpen'}}</div>
|
||||||
<div class="footer__btn footer__btn-help"><i class="fa fa-question"></i></div>
|
<div class="footer__btn footer__btn-help" title="{{res 'footerTitleHelp'}}" tip-placement="top"><i class="fa fa-question"></i></div>
|
||||||
<div class="footer__btn footer__btn-settings">
|
<div class="footer__btn footer__btn-settings" title="{{res 'footerTitleSettings'}}" tip-placement="top">
|
||||||
{{#if updateAvailable}}
|
{{#if updateAvailable}}
|
||||||
<i class="fa fa-bell footer__update-icon"></i>
|
<i class="fa fa-bell footer__update-icon"></i>
|
||||||
{{else}}
|
{{else}}
|
||||||
<i class="fa fa-cog"></i>
|
<i class="fa fa-cog"></i>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer__btn footer__btn-generate"><i class="fa fa-bolt"></i></div>
|
<div class="footer__btn footer__btn-generate" title="{{res 'footerTitleGen'}}" tip-placement="top"><i class="fa fa-bolt"></i></div>
|
||||||
<div class="footer__btn footer__btn-lock"><i class="fa fa-lock"></i></div>
|
<div class="footer__btn footer__btn-lock" title="{{res 'footerTitleLock'}}" tip-placement="top-left"><i class="fa fa-sign-out"></i></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="icon-select">
|
<div class="icon-select">
|
||||||
<div class="icon-select__items">
|
<div class="icon-select__items">
|
||||||
{{#each icons as |icon ix|}}
|
{{#each icons as |icon ix|}}
|
||||||
<i class="fa fa-{{icon}} icon-select__icon {{#ifeq ix sel}}icon-select__icon--active{{/ifeq}}" data-val="{{ix}}"></i>
|
<i class="fa fa-{{icon}} icon-select__icon {{#ifeq ix ../sel}}icon-select__icon--active{{/ifeq}}" data-val="{{ix}}"></i>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-select__items icon-select__items--custom">
|
<div class="icon-select__items icon-select__items--custom">
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
<i class="fa fa-ellipsis-h"></i>
|
<i class="fa fa-ellipsis-h"></i>
|
||||||
</span>
|
</span>
|
||||||
{{#each customIcons as |icon ci|}}
|
{{#each customIcons as |icon ci|}}
|
||||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci sel}}icon-select__icon--active{{/ifeq}}"
|
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci ../sel}}icon-select__icon--active{{/ifeq}}"
|
||||||
data-val="{{ci}}">
|
data-val="{{ci}}">
|
||||||
<img src="{{{icon}}}" />
|
<img src="{{{icon}}}" />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
<li><a href="http://electron.atom.io/" target="_blank">electron</a><span class="muted-color">, cross-platform desktop apps framework</span></li>
|
<li><a href="http://electron.atom.io/" target="_blank">electron</a><span class="muted-color">, cross-platform desktop apps framework</span></li>
|
||||||
<li><a href="http://backbonejs.org/" target="_blank">backbone</a><span class="muted-color">, JavaScript framework</span></li>
|
<li><a href="http://backbonejs.org/" target="_blank">backbone</a><span class="muted-color">, JavaScript framework</span></li>
|
||||||
<li><a href="http://underscorejs.org/" target="_blank">underscore</a><span class="muted-color">, utility-belt library for JavaScript</span></li>
|
<li><a href="http://underscorejs.org/" target="_blank">underscore</a><span class="muted-color">, utility-belt library for JavaScript</span></li>
|
||||||
<li><a href="http://zeptojs.com/" target="_blank">zepto.js</a><span class="muted-color">, a minimalist JavaScript library for modern browsers,
|
<li><a href="https://jquery.com/" target="_blank">jQuery</a><span class="muted-color">, fast, small, and feature-rich JavaScript library</span></li>
|
||||||
with a jQuery-compatible API</span></li>
|
|
||||||
<li><a href="http://handlebarsjs.com/" target="_blank">handlebars</a><span class="muted-color">, semantic templates</span></li>
|
<li><a href="http://handlebarsjs.com/" target="_blank">handlebars</a><span class="muted-color">, semantic templates</span></li>
|
||||||
<li><a href="https://github.com/dropbox/dropbox-js" target="_blank">dropbox-js</a><span class="muted-color">, unofficial JavaScript library for
|
<li><a href="https://github.com/dropbox/dropbox-js" target="_blank">dropbox-js</a><span class="muted-color">, unofficial JavaScript library for
|
||||||
the Dropbox Core API</span></li>
|
the Dropbox Core API</span></li>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<label for="settings__general-theme">{{res 'setGenTheme'}}:</label>
|
<label for="settings__general-theme">{{res 'setGenTheme'}}:</label>
|
||||||
<select class="settings__general-theme settings__select input-base" id="settings__general-theme">
|
<select class="settings__general-theme settings__select input-base" id="settings__general-theme">
|
||||||
{{#each themes as |name key|}}
|
{{#each themes as |name key|}}
|
||||||
<option value="{{key}}" {{#ifeq key activeTheme}}selected{{/ifeq}}>{{name}}</option>
|
<option value="{{key}}" {{#ifeq key ../activeTheme}}selected{{/ifeq}}>{{name}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
BIN
app/touchicon.png
Normal file
BIN
app/touchicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -24,7 +24,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"backbone": "~1.2.3",
|
"backbone": "~1.2.3",
|
||||||
"baron": "~1.0.1",
|
"baron": "~2.0.1",
|
||||||
"bourbon": "~4.2.5",
|
"bourbon": "~4.2.5",
|
||||||
"dropbox": "antelle/dropbox-js#0.10.6",
|
"dropbox": "antelle/dropbox-js#0.10.6",
|
||||||
"font-awesome": "~4.4.0",
|
"font-awesome": "~4.4.0",
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"kdbxweb": "~0.3.3",
|
"kdbxweb": "~0.3.3",
|
||||||
"normalize.css": "~3.0.3",
|
"normalize.css": "~3.0.3",
|
||||||
"pikaday": "~1.3.3",
|
"pikaday": "~1.3.3",
|
||||||
"zepto": "~1.1.6",
|
"FileSaver.js": "eligrey/FileSaver.js",
|
||||||
"FileSaver.js": "eligrey/FileSaver.js"
|
"jquery": "~2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,9 @@ app.minimizeApp = function() {
|
||||||
appIcon.setToolTip('KeeWeb');
|
appIcon.setToolTip('KeeWeb');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
app.getMainWindow = function() {
|
||||||
|
return mainWindow;
|
||||||
|
};
|
||||||
|
|
||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "KeeWeb",
|
"name": "KeeWeb",
|
||||||
"version": "0.6.1",
|
"version": "1.0.0",
|
||||||
"description": "KeePass web app",
|
"description": "KeePass web app",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"repository": "https://github.com/antelle/keeweb",
|
"repository": "https://github.com/antelle/keeweb",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "keeweb",
|
"name": "keeweb",
|
||||||
"version": "0.6.1",
|
"version": "1.0.0",
|
||||||
"description": "KeePass web app",
|
"description": "KeePass web app",
|
||||||
"main": "Gruntfile.js",
|
"main": "Gruntfile.js",
|
||||||
"repository": "https://github.com/antelle/keeweb",
|
"repository": "https://github.com/antelle/keeweb",
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
Release notes
|
Release notes
|
||||||
-------------
|
-------------
|
||||||
|
##### v1.0.0 (2016-02-12)
|
||||||
|
Performance, stability and quality improvements
|
||||||
|
`+` track changes in local files
|
||||||
|
`+` mobile layout made more convenient
|
||||||
|
`+` command-line option to disable updater
|
||||||
|
`+` using system proxy settings for updater
|
||||||
|
`+` webapp icon for touch devices
|
||||||
|
`-` #80: prevent data loss on group move
|
||||||
|
`-` issues with clipboard clear fixed
|
||||||
|
|
||||||
##### v0.6.1 (2016-02-02)
|
##### v0.6.1 (2016-02-02)
|
||||||
App moved to app.keeweb.info
|
App moved to app.keeweb.info
|
||||||
|
|
||||||
##### v0.6.0 (2016-01-19)
|
##### v0.6.0 (2016-01-19)
|
||||||
Improvements
|
Improvements
|
||||||
|
|
Loading…
Reference in New Issue
Block a user