1
0
mirror of https://github.com/keeweb/keeweb.git synced 2024-06-22 07:16:38 +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 dt = new Date().toISOString().replace(/T.*/, '');
var electronVersion = '0.36.4';
var appUpdateMinVersion = '0.5.0';
var minElectronVersionForUpdate = '0.32.0';
function replaceFont(css) {
css.walkAtRules('font-face', function (rule) {
@ -73,6 +73,11 @@ module.exports = function(grunt) {
dest: 'tmp/favicon.png',
nonull: true
},
touchicon: {
src: 'app/touchicon.png',
dest: 'tmp/touchicon.png',
nonull: true
},
fonts: {
src: 'bower_components/font-awesome/fonts/fontawesome-webfont.*',
dest: 'tmp/fonts/',
@ -154,7 +159,7 @@ module.exports = function(grunt) {
options: {
replacements: [
{ 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' }
@ -172,7 +177,7 @@ module.exports = function(grunt) {
js: {
entry: {
app: 'app',
vendor: ['zepto', 'jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'dropbox', 'pikaday', 'filesaver']
vendor: ['jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'dropbox', 'pikaday', 'filesaver']
},
output: {
path: 'tmp/js',
@ -191,8 +196,7 @@ module.exports = function(grunt) {
backbone: 'backbone/backbone-min.js',
underscore: 'underscore/underscore-min.js',
_: 'underscore/underscore-min.js',
zepto: 'zepto/zepto.min.js',
jquery: 'zepto/zepto.min.js',
jquery: 'jquery/dist/jquery.min.js',
hbs: 'handlebars/runtime.js',
kdbxweb: 'kdbxweb/dist/kdbxweb.js',
dropbox: 'dropbox/lib/dropbox.min.js',
@ -213,7 +217,6 @@ module.exports = function(grunt) {
{ pattern: /@@DATE/g, replacement: function() { return dt; } },
{ 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: /pikadat\.js$/, loader: 'uglify' },
{ test: /handlebars/, loader: 'strip-sourcemap-loader' }
@ -373,6 +376,7 @@ module.exports = function(grunt) {
'jshint',
'copy:html',
'copy:favicon',
'copy:touchicon',
'copy:fonts',
'webpack',
'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.
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
@ -15,8 +15,7 @@ Twitter: [kee_web](https://twitter.com/kee_web)
# 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.
Please see [TODO](https://github.com/antelle/keeweb/wiki/TODO) for more details.
Project roadmap with planned features and approximate schedule is on [TODO](https://github.com/antelle/keeweb/wiki/TODO) page.
# Self-hosting

View File

@ -5,6 +5,7 @@
<title>KeeWeb</title>
<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="apple-touch-icon" sizes="192x192" href="touchicon.png?__inline=true">
<link rel="stylesheet" href="css/main.css?__inline=true" />
<script src="js/vendor.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');
var CopyPaste = {
tryCopy: function() {
try {
var success = document.execCommand('copy');
if (success) {
this.copied();
simpleCopy: !!Launcher,
copy: function(text) {
if (Launcher) {
Launcher.setClipboardText(text);
var clipboardSeconds = AppSettingsModel.instance.get('clipboardSeconds');
if (clipboardSeconds > 0) {
setTimeout(function () {
if (Launcher.getClipboardText() === text) {
Launcher.clearClipboardText();
}
}, clipboardSeconds * 1000);
}
return success;
} catch (e) {
return {success: true, seconds: clipboardSeconds};
} else {
try {
if (document.execCommand('copy')) {
return {success: true};
}
} catch (e) { }
return false;
}
},
@ -37,22 +49,6 @@ var CopyPaste = {
'copy cut paste': function() { setTimeout(function() { hiddenInput.blur(); }, 0); },
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() {
this.req('remote').getCurrentWindow().openDevTools();
},
getAppVersion: function() {
return this.remReq('app').getVersion();
},
getSaveFileName: function(defaultPath, cb) {
if (defaultPath) {
var homePath = this.remReq('app').getPath('userDesktop');
@ -53,6 +50,16 @@ if (window.process && window.process.versions && window.process.versions.electro
deleteFile: function(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) {
e.returnValue = false;
return false;
@ -76,6 +83,9 @@ if (window.process && window.process.versions && window.process.versions.electro
cancelRestart: function() {
this.restartPending = false;
},
setClipboardText: function(text) {
return this.req('clipboard').writeText(text);
},
getClipboardText: function() {
return this.req('clipboard').readText();
},
@ -87,6 +97,18 @@ if (window.process && window.process.versions && window.process.versions.electro
},
canMinimize: function() {
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() {

View File

@ -28,43 +28,58 @@ var Transport = {
logger.info('GET ' + config.url);
var opts = Launcher.req('url').parse(config.url);
opts.headers = { 'User-Agent': navigator.userAgent };
Launcher.req(proto).get(opts, function(res) {
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); });
Launcher.resolveProxy(config.url, function(proxy) {
logger.info('Request to ' + config.url + ' ' + (proxy ? 'using proxy ' + proxy.host + ':' + proxy.port : 'without proxy'));
if (proxy) {
opts.headers.Host = opts.host;
opts.host = proxy.host;
opts.port = proxy.port;
opts.path = config.url;
}
Launcher.req(proto).get(opts, function (res) {
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 {
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);
});
config.error('HTTP status ' + res.statusCode);
}
} else if (res.headers.location && [301, 302].indexOf(res.statusCode) >= 0) {
if (config.noRedirect) {
return config.error('Too many redirects');
}).on('error', function (e) {
logger.error('Cannot GET ' + config.url, e);
if (tmpFile) {
fs.unlink(tmpFile);
}
config.url = res.headers.location;
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);
config.error(e);
});
});
}
};

View File

@ -18,9 +18,10 @@ var Updater = {
UpdateCheckFiles: ['index.html', 'app.js'],
nextCheckTimeout: null,
updateCheckDate: new Date(0),
enabled: Launcher && Launcher.updaterEnabled(),
getAutoUpdateType: function() {
if (!Launcher) {
if (!this.enabled) {
return false;
}
var autoUpdate = AppSettingsModel.instance.get('autoUpdate');
@ -65,10 +66,10 @@ var Updater = {
},
check: function(startedByUser) {
if (!Launcher || this.updateInProgress()) {
if (!startedByUser) {
return;
}
if (this.checkManualDownload()) {
if (!this.enabled || this.updateInProgress()) {
return;
}
UpdateModel.instance.set('status', 'checking');
@ -85,7 +86,7 @@ var Updater = {
}
logger.info('Checking for update...');
Transport.httpGet({
url: Links.WebApp + 'manifest.appcache',
url: Links.Manifest,
utf8: true,
success: function(data) {
var dt = new Date();
@ -111,6 +112,9 @@ var Updater = {
});
UpdateModel.instance.save();
that.scheduleNextCheck();
if (!that.canAutoUpdate()) {
return;
}
if (prevLastVersion === UpdateModel.instance.get('lastVersion') &&
UpdateModel.instance.get('updateStatus') === 'ready') {
logger.info('Waiting for the user to apply downloaded update');
@ -118,7 +122,7 @@ var Updater = {
}
if (!startedByUser && that.getAutoUpdateType() === 'install') {
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');
}
},
@ -135,16 +139,41 @@ var Updater = {
});
},
checkManualDownload: function() {
if (+Launcher.getAppVersion().split('.')[1] <= 2) {
UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true });
return true;
canAutoUpdate: function() {
var minLauncherVersion = UpdateModel.instance.get('lastCheckUpdMin');
if (minLauncherVersion) {
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) {
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');
return;
}

View File

@ -7,7 +7,8 @@ var Links = {
License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt',
UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes',
SelfHostedDropbox: 'https://github.com/antelle/keeweb#self-hosting'
SelfHostedDropbox: 'https://github.com/antelle/keeweb#self-hosting',
Manifest: 'https://antelle.github.io/keeweb/manifest.appcache'
};
module.exports = Links;

View File

@ -3,7 +3,8 @@
var Timeouts = {
AutoSync: 30 * 1000 * 60,
CopyTip: 1500,
AutoHideHint: 3000
AutoHideHint: 3000,
FileChangeSync: 3000
};
module.exports = Timeouts;

View File

@ -9,7 +9,11 @@ var isEnabled = FeatureDetector.isDesktop();
var Scrollable = {
createScroll: function(opts) {
opts.$ = Backbone.$;
//opts.cssGuru = true;
if (isEnabled) {
if (this.scroll) {
this.removeScroll();
}
this.scroll = baron(opts);
}
this.scroller = this.$el.find('.scroller');
@ -17,6 +21,18 @@ var Scrollable = {
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() {
// TODO: check size on window resize
//if (this.checkSize && (!e || e.source === 'window')) {

View File

@ -42,18 +42,22 @@ _.extend(Backbone.View.prototype, {
},
renderTemplate: function(model, replace) {
if (replace) {
this.$el.html('');
}
var el = $(this.template(model));
if (!this._elAppended || replace) {
this.$el.append(el);
this._elAppended = true;
if (replace && replace.plain) {
this.$el.html(this.template(model));
} 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(el);
Tip.createTips(this.$el);
},
_parentRemove: Backbone.View.prototype.remove,

View File

@ -11,6 +11,7 @@ var Backbone = require('backbone'),
FileModel = require('./file-model'),
FileInfoModel = require('./file-info-model'),
Storage = require('../storage'),
Timeouts = require('../const/timeouts'),
IdGenerator = require('../util/id-generator'),
Logger = require('../util/logger');
@ -108,7 +109,11 @@ var AppModel = Backbone.Model.extend({
},
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.menu.groupsSection.removeAllItems();
this.menu.tagsSection.set('scrollable', false);
@ -119,6 +124,8 @@ var AppModel = Backbone.Model.extend({
},
closeFile: function(file) {
file.close();
this.fileClosed(file);
this.files.remove(file);
this.updateTags();
this.menu.groupsSection.removeByFile(file);
@ -352,13 +359,14 @@ var AppModel = Backbone.Model.extend({
}
var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid();
file.set('cacheId', cacheId);
if (updateCacheOnSuccess && params.storage !== 'file') {
if (updateCacheOnSuccess) {
logger.info('Save loaded file to cache');
Storage.cache.save(cacheId, params.fileData);
}
var rev = params.rev || fileInfo && fileInfo.get('rev');
that.addToLastOpenFiles(file, rev);
that.addFile(file);
that.fileOpened(file);
});
},
@ -381,6 +389,21 @@ var AppModel = Backbone.Model.extend({
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) {
Storage.cache.remove(id);
this.fileInfos.remove(id);
@ -425,7 +448,7 @@ var AppModel = Backbone.Model.extend({
file.setSyncProgress();
var complete = function(err, savedToCache) {
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.set('cacheId', fileInfo.id);
fileInfo.set({
@ -452,7 +475,7 @@ var AppModel = Backbone.Model.extend({
file.getData(function(data, err) {
if (err) { return complete(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);
});
});
@ -464,10 +487,10 @@ var AppModel = Backbone.Model.extend({
}
logger.info('Load from storage, attempt ' + loadLoops);
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); }
file.mergeOrUpdate(data, options.remoteKey, function(err) {
logger.info('Merge complete', err);
logger.info('Merge complete', err || 'no error');
that.refresh();
if (err) {
if (err.code === 'InvalidKey') {
@ -484,7 +507,7 @@ var AppModel = Backbone.Model.extend({
if (file.get('modified')) {
logger.info('Updated sync date, saving modified file to cache and storage');
saveToCacheAndStorage();
} else if (file.get('dirty') && storage !== 'file') {
} else if (file.get('dirty')) {
logger.info('Saving not modified dirty file to cache');
Storage.cache.save(fileInfo.id, data, function (err) {
if (err) { return complete(err); }
@ -503,8 +526,8 @@ var AppModel = Backbone.Model.extend({
logger.info('Save to cache and storage');
file.getData(function(data, err) {
if (err) { return complete(err); }
if (!file.get('dirty') || storage === 'file') {
logger.info('Save to storage, skip cache because not dirty or file storage');
if (!file.get('dirty')) {
logger.info('Save to storage, skip cache because not dirty');
saveToStorage(data);
} else {
logger.info('Saving to cache');
@ -527,9 +550,6 @@ var AppModel = Backbone.Model.extend({
logger.info('Error saving data to storage');
complete(err);
} else {
if (storage === 'file') {
Storage.cache.remove(fileInfo.id);
}
if (stat && stat.rev) {
logger.info('Update rev in file info');
fileInfo.set('rev', stat.rev);
@ -540,55 +560,42 @@ var AppModel = Backbone.Model.extend({
}
}, fileInfo.get('rev'));
};
if (options.reload) {
logger.info('Saved to cache');
loadFromStorageAndMerge();
} else if (storage === 'file') {
if (file.get('modified') || file.get('path') !== path) {
logger.info('Save modified file to storage');
saveToCacheAndStorage();
} else {
logger.info('Skip not modified file');
complete();
}
} else {
logger.info('Stat file');
Storage[storage].stat(path, function (err, stat) {
if (err) {
if (err.notFound) {
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();
}
logger.info('Stat file');
Storage[storage].stat(path, function (err, stat) {
if (err) {
if (err.notFound) {
logger.info('File does not exist in storage, creating');
saveToCacheAndStorage();
} else if (file.get('dirty')) {
logger.info('Stat error, dirty, save to cache', err || 'no error');
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 || 'no error');
complete(err);
});
}
});
} else {
logger.info('Found new version, loading from storage');
loadFromStorageAndMerge();
logger.info('Stat error, not dirty', err || 'no error');
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();
if (object instanceof GroupModel) {
for (var parent = this; parent; parent = parent.parentGroup) {
if (object === parent) {
return;
}
}
if (this.group.groups.indexOf(object.group) >= 0) {
return;
}

View File

@ -5,6 +5,8 @@ var Launcher = require('../comp/launcher'),
var logger = new Logger('storage-file');
var fileWatchers = {};
var StorageFile = {
name: 'file',
enabled: !!Launcher,
@ -14,25 +16,95 @@ var StorageFile = {
var ts = logger.ts();
try {
var data = Launcher.readFile(path);
logger.debug('Loaded', path, logger.ts(ts));
if (callback) { callback(null, data.buffer); }
var rev = Launcher.statFile(path).mtime.getTime().toString();
logger.debug('Loaded', path, rev, logger.ts(ts));
if (callback) { callback(null, data.buffer, { rev: rev }); }
} catch (e) {
logger.error('Error reading local file', path, e);
if (callback) { callback(e, null); }
}
},
save: function(path, data, callback) {
logger.debug('Save', path);
stat: function(path, callback) {
logger.debug('Stat', path);
var ts = logger.ts();
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);
var newRev = Launcher.statFile(path).mtime.getTime().toString();
logger.debug('Saved', path, logger.ts(ts));
if (callback) { callback(); }
if (callback) { callback(undefined, { rev: newRev }); }
} catch (e) {
logger.error('Error writing local file', path, 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',
footerSyncError: 'Sync error',
footerTitleHelp: 'Help',
footerTitleSettings: 'Settings',
footerTitleGen: 'Generate',
footerTitleLock: 'Lock',
genLen: 'Length',
grpTitle: 'Group',
@ -147,7 +151,7 @@ var Locale = {
appSecWarn: 'Not Secure!',
appSecWarnBody1: 'You have loaded this app with insecure connection. ' +
'Someone may be watching you and stealing your passwords. ' +
'We strongly advice you to stop, unless you clearly understand what you\'re doing.',
'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.',
appSecWarnBtn: 'I understand the risks, continue',
appUnsavedWarn: 'Unsaved changes!',
@ -168,7 +172,7 @@ var Locale = {
setGenUpdate: 'Update',
setGenNewVersion: 'New app version was released and downloaded',
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 ' +
'but auto-upgrading from your version is impossible.',
setGenDownloadUpdate: 'Download update',
@ -305,7 +309,7 @@ var Locale = {
dropboxFullBody: 'Your Dropbox is full, there\'s no space left anymore.',
dropboxRateLimitedBody: 'Too many requests to Dropbox have been made by this app. Please, try again later.',
dropboxNetError: 'Dropbox Sync Network Error',
dropboxNetErrorBody: 'Network error occured during Dropbox sync. Please, check your connection and try again.',
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: ',
dropboxErrorRepeatBody: 'Something went wrong during Dropbox sync. Please, try again later. Error: ',

View File

@ -1,6 +1,7 @@
'use strict';
var FeatureDetector = require('./feature-detector');
var Backbone = require('backbone'),
FeatureDetector = require('./feature-detector');
var Tip = function(el, config) {
this.el = el;
@ -10,6 +11,7 @@ var Tip = function(el, config) {
this.tipEl = null;
this.showTimeout = null;
this.hideTimeout = null;
this.hide = this.hide.bind(this);
};
Tip.enabled = FeatureDetector.isDesktop();
@ -27,6 +29,7 @@ Tip.prototype.show = function() {
if (!Tip.enabled) {
return;
}
Backbone.on('page-geometry', this.hide);
if (this.tipEl) {
this.tipEl.remove();
if (this.hideTimeout) {
@ -44,11 +47,16 @@ Tip.prototype.show = function() {
}
var top, left;
var offset = 10;
var sideOffset = 10;
switch (placement) {
case 'top':
top = rect.top - tipRect.height - offset;
left = rect.left + rect.width / 2 - tipRect.width / 2;
break;
case 'top-left':
top = rect.top - tipRect.height - offset;
left = rect.left + rect.width / 2 - tipRect.width + sideOffset;
break;
case 'bottom':
top = rect.bottom + offset;
left = rect.left + rect.width / 2 - tipRect.width / 2;
@ -70,6 +78,7 @@ Tip.prototype.hide = function() {
this.tipEl.remove();
this.tipEl = null;
}
Backbone.off('page-geometry', this.hide);
};
Tip.prototype.mouseenter = function() {
@ -129,11 +138,8 @@ Tip.createTips = function(container) {
return;
}
container.find('[title]').each(function(ix, el) {
if (!el._tip) {
var tip = new Tip($(el));
tip.init();
el._tip = tip;
}
var tip = new Tip($(el));
tip.init();
});
};

View File

@ -408,10 +408,13 @@ var AppView = Backbone.View.extend({
},
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();
if (firstFile) {
var fileInfo = this.model.fileInfos.getMatch(firstFile.get('storage'), firstFile.get('name'), firstFile.get('path'));
if (!fileToShow) {
fileToShow = this.model.fileInfos.getLast();
}
if (fileToShow) {
var fileInfo = this.model.fileInfos.getMatch(fileToShow.get('storage'), fileToShow.get('name'), fileToShow.get('path'));
if (fileInfo) {
this.views.open.showOpenFileInfo(fileInfo);
}

View File

@ -78,6 +78,7 @@ var DetailsView = Backbone.View.extend({
},
render: function () {
this.removeScroll();
this.removeFieldViews();
if (this.views.sub) {
this.views.sub.remove();
@ -272,10 +273,13 @@ var DetailsView = Backbone.View.extend({
if (!window.getSelection().toString()) {
var pw = this.model.password;
var password = pw.isProtected ? pw.getText() : pw;
CopyPaste.createHiddenInput(password);
var clipboardTime = CopyPaste.copied();
if (!this.passCopyTip) {
if (!CopyPaste.simpleCopy) {
CopyPaste.createHiddenInput(password);
}
var copyRes = CopyPaste.copy(password);
if (copyRes && !this.passCopyTip) {
var passLabel = this.passEditView.labelEl;
var clipboardTime = copyRes.seconds;
var msg = clipboardTime ? Locale.detPassCopiedTime.replace('{}', clipboardTime)
: Locale.detPassCopied;
var tip = new Tip(passLabel, { title: msg, placement: 'right', fast: true });

View File

@ -41,14 +41,17 @@ var FieldView = Backbone.View.extend({
return;
}
CopyPaste.createHiddenInput(textValue, box);
//CopyPaste.tryCopy(); // maybe Apple will ever support this?
//CopyPaste.copy(); // maybe Apple will ever support this?
return;
}
if (field) {
var value = this.value || '';
if (value && value.isProtected) {
CopyPaste.createHiddenInput(value.getText());
CopyPaste.tryCopy();
var text = value.getText();
if (!CopyPaste.simpleCopy) {
CopyPaste.createHiddenInput(text);
}
CopyPaste.copy(text);
return;
}
}
@ -57,7 +60,7 @@ var FieldView = Backbone.View.extend({
range.selectNodeContents(this.valueEl[0]);
selection.removeAllRanges();
selection.addRange(range);
if (CopyPaste.tryCopy()) {
if (CopyPaste.copy(this.valueEl.text())) {
selection.removeAllRanges();
}
},

View File

@ -4,7 +4,6 @@ var Backbone = require('backbone'),
Keys = require('../const/keys'),
KeyHandler = require('../comp/key-handler'),
GeneratorView = require('./generator-view'),
Tip = require('../util/tip'),
UpdateModel = require('../models/update-model');
var FooterView = Backbone.View.extend({
@ -33,11 +32,10 @@ var FooterView = Backbone.View.extend({
},
render: function () {
this.$el.html(this.template({
this.renderTemplate({
files: this.model.files,
updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
}));
Tip.createTips(this.$el);
}, { plain: true });
return this;
},

View File

@ -74,7 +74,7 @@ var GeneratorView = Backbone.View.extend({
range.selectNodeContents(this.resultEl[0]);
selection.removeAllRanges();
selection.addRange(range);
CopyPaste.tryCopy();
CopyPaste.copy(this.password);
this.trigger('result', this.password);
this.remove();
}

View File

@ -2,8 +2,7 @@
var Backbone = require('backbone'),
Scrollable = require('../mixins/scrollable'),
IconSelectView = require('./icon-select-view'),
Tip = require('../util/tip');
IconSelectView = require('./icon-select-view');
var GrpView = Backbone.View.extend({
template: require('templates/grp.hbs'),
@ -23,14 +22,13 @@ var GrpView = Backbone.View.extend({
render: function() {
this.removeSubView();
if (this.model) {
this.$el.html(this.template({
this.renderTemplate({
title: this.model.get('title'),
icon: this.model.get('icon') || 'folder',
customIcon: this.model.get('customIcon'),
enableSearching: this.model.get('enableSearching') !== false,
readonly: this.model.get('top')
}));
Tip.createTips(this.$el);
}, { plain: true });
if (!this.model.get('title')) {
this.$el.find('#grp__field-title').focus();
}

View File

@ -57,7 +57,7 @@ var SettingsGeneralView = Backbone.View.extend({
idleMinutes: AppSettingsModel.instance.get('idleMinutes'),
minimizeOnClose: AppSettingsModel.instance.get('minimizeOnClose'),
devTools: Launcher && Launcher.devTools,
canAutoUpdate: !!Launcher,
canAutoUpdate: Updater.enabled,
canMinimize: Launcher && Launcher.canMinimize(),
lockOnMinimize: Launcher && AppSettingsModel.instance.get('lockOnMinimize'),
tableView: AppSettingsModel.instance.get('tableView'),
@ -66,7 +66,7 @@ var SettingsGeneralView = Backbone.View.extend({
updateInProgress: Updater.updateInProgress(),
updateInfo: this.getUpdateInfo(),
updateWaitingReload: updateReady && !Launcher,
showUpdateBlock: Launcher && !updateManual,
showUpdateBlock: Updater.enabled && !updateManual,
updateReady: updateReady,
updateFound: updateFound,
updateManual: updateManual,
@ -91,7 +91,8 @@ var SettingsGeneralView = Backbone.View.extend({
return errMsg;
case 'ok':
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;
} else {
msg += Locale.setGenNewVer.replace('{}', UpdateModel.instance.get('lastVersion')) + ' ' +

View File

@ -12,6 +12,8 @@
display: block;
padding-bottom: $base-padding-v;
cursor: pointer;
line-height: $mobile-back-button-height;
height: $mobile-back-button-height;
>i { margin-right: $base-padding-h; }
}
}
@ -130,10 +132,6 @@
@include flex-wrap(wrap);
overflow-x: hidden;
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; }
}
@ -197,6 +195,7 @@
line-height: $details-field-line-height;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 20px;
.details__field--editable & {
border-radius: $base-border-radius;
&:hover {
@ -367,6 +366,7 @@
@include display(flex);
@include align-items(stretch);
@include flex-direction(row);
@include flex-shrink(0);
@include justify-content(flex-start);
margin-top: $base-padding-v;

View File

@ -21,11 +21,6 @@
@include flex(1);
@include align-self(stretch);
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 {
width: 100% !important;
max-width: 100% !important;
@ -124,7 +119,7 @@
}
&-icon {
width: 14px;
width: 16px;
height: 14px;
&--custom {
vertical-align: text-bottom;

View File

@ -11,6 +11,7 @@
@include display(flex);
@include align-items(stretch);
@include flex-direction(row);
@include flex-shrink(0);
.open--drag & { display: none; }
}
@ -46,6 +47,7 @@
@include flex-direction(row);
@include justify-content(flex-start);
@include align-items(stretch);
@include flex-shrink(0);
margin-bottom: $base-padding-v;
}
&-enter-btn, &-opening-icon {
@ -126,6 +128,7 @@
@include flex-direction(row);
@include justify-content(flex-start);
@include align-items(baseline);
@include flex-shrink(0);
.open:not(.open--opening) & {
@include area-selectable;
}

View File

@ -12,9 +12,6 @@
>.scroller {
@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 {
@ -38,6 +35,8 @@
&-pre, &-post { display: none; }
cursor: pointer;
@include mobile {
line-height: $mobile-back-button-height;
height: $mobile-back-button-height;
padding-bottom: $base-padding-v;
>i { margin-right: $base-padding-h; }
&-pre { display: inline; }

View File

@ -18,6 +18,7 @@ $small-spacing: $base-spacing / 2;
$base-z-index: 0;
$base-padding-v: .4em;
$base-padding-h: .8em;
$mobile-back-button-height: 3em;
$base-padding: $base-padding-v $base-padding-h;
$medium-padding: .8em 1em;
$base-padding-px: 4px 8px;

View File

@ -1,10 +1,5 @@
&.icon-select {
@include display(flex);
@include align-items(stretch);
@include flex-direction(column);
@include justify-content(flex-start);
&__items {
@include flex(1 1 auto);
@include display(flex);
@include align-items(flex-start);
@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 {
overflow-y: scroll;
height: 100%;
@-moz-document url-prefix() {
@include scrollbar-padding-hack();
}
&::-webkit-scrollbar {
width: 0;
}

View File

@ -27,6 +27,8 @@
$arrow-size-small: 10px 8px;
$arrow-size-large: 12px 9px;
$arrow-offset-small: 10px;
$arrow-offset-large: 11px;
&.tip--bottom:after {
@include position(absolute, - nth($arrow-size-small, 2) null null 50%);
@ -38,6 +40,11 @@
@include transform(translate(-50%, 0));
@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 {
@include position(absolute, 50% null null 100%);
@include transform(translate(0, -50%));
@ -59,6 +66,11 @@
@include transform(translate(-50%, 0));
@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 {
@include position(absolute, 50% null null 100%);
@include transform(translate(0, -50%));

View File

@ -13,14 +13,14 @@
</div>
{{/each}}
<div class="footer__db footer__db--dimmed footer__db--expanded footer__db-open"><i class="fa fa-plus"></i> {{res 'footerOpen'}}</div>
<div class="footer__btn footer__btn-help"><i class="fa fa-question"></i></div>
<div class="footer__btn footer__btn-settings">
<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" title="{{res 'footerTitleSettings'}}" tip-placement="top">
{{#if updateAvailable}}
<i class="fa fa-bell footer__update-icon"></i>
{{else}}
<i class="fa fa-cog"></i>
{{/if}}
</div>
<div class="footer__btn footer__btn-generate"><i class="fa fa-bolt"></i></div>
<div class="footer__btn footer__btn-lock"><i class="fa fa-lock"></i></div>
<div 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" title="{{res 'footerTitleLock'}}" tip-placement="top-left"><i class="fa fa-sign-out"></i></div>
</div>

View File

@ -1,7 +1,7 @@
<div class="icon-select">
<div class="icon-select__items">
{{#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}}
</div>
<div class="icon-select__items icon-select__items--custom">
@ -17,7 +17,7 @@
<i class="fa fa-ellipsis-h"></i>
</span>
{{#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}}">
<img src="{{{icon}}}" />
</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://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://zeptojs.com/" target="_blank">zepto.js</a><span class="muted-color">, a minimalist JavaScript library for modern browsers,
with a jQuery-compatible API</span></li>
<li><a href="https://jquery.com/" target="_blank">jQuery</a><span class="muted-color">, fast, small, and feature-rich JavaScript library</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
the Dropbox Core API</span></li>

View File

@ -41,7 +41,7 @@
<label for="settings__general-theme">{{res 'setGenTheme'}}:</label>
<select class="settings__general-theme settings__select input-base" id="settings__general-theme">
{{#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}}
</select>
</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,
"dependencies": {
"backbone": "~1.2.3",
"baron": "~1.0.1",
"baron": "~2.0.1",
"bourbon": "~4.2.5",
"dropbox": "antelle/dropbox-js#0.10.6",
"font-awesome": "~4.4.0",
@ -32,7 +32,7 @@
"kdbxweb": "~0.3.3",
"normalize.css": "~3.0.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');
}
};
app.getMainWindow = function() {
return mainWindow;
};
function createMainWindow() {
mainWindow = new BrowserWindow({

View File

@ -1,6 +1,6 @@
{
"name": "KeeWeb",
"version": "0.6.1",
"version": "1.0.0",
"description": "KeePass web app",
"main": "main.js",
"repository": "https://github.com/antelle/keeweb",

View File

@ -1,6 +1,6 @@
{
"name": "keeweb",
"version": "0.6.1",
"version": "1.0.0",
"description": "KeePass web app",
"main": "Gruntfile.js",
"repository": "https://github.com/antelle/keeweb",

View File

@ -1,7 +1,17 @@
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)
App moved to app.keeweb.info
App moved to app.keeweb.info
##### v0.6.0 (2016-01-19)
Improvements