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:
commit
186eb7b359
16
Gruntfile.js
16
Gruntfile.js
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
var Timeouts = {
|
||||
AutoSync: 30 * 1000 * 60,
|
||||
CopyTip: 1500,
|
||||
AutoHideHint: 3000
|
||||
AutoHideHint: 3000,
|
||||
FileChangeSync: 3000
|
||||
};
|
||||
|
||||
module.exports = Timeouts;
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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: ',
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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')) + ' ' +
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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%));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
BIN
app/touchicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,9 @@ app.minimizeApp = function() {
|
|||
appIcon.setToolTip('KeeWeb');
|
||||
}
|
||||
};
|
||||
app.getMainWindow = function() {
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
function createMainWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user