1
0
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:
Antelle 2016-02-12 20:41:29 +03:00
commit 186eb7b359
41 changed files with 426 additions and 238 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

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

View File

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

View File

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

View File

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

View File

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