Merge branch 'no-auto-update'

Conflicts:
	README.md
This commit is contained in:
Antelle 2015-11-14 21:05:56 +03:00
commit e4c9f1d7c9
26 changed files with 766 additions and 120 deletions

View File

@ -3,6 +3,9 @@
var fs = require('fs'),
path = require('path');
/* jshint node:true */
/* jshint browser:false */
var StringReplacePlugin = require('string-replace-webpack-plugin');
module.exports = function(grunt) {
@ -55,8 +58,8 @@ module.exports = function(grunt) {
},
clean: {
dist: ['dist', 'tmp'],
desktop_dist: ['dist/desktop'],
desktop_tmp: ['tmp/desktop']
'desktop_dist': ['dist/desktop'],
'desktop_tmp': ['tmp/desktop']
},
copy: {
html: {
@ -76,6 +79,13 @@ module.exports = function(grunt) {
expand: true,
flatten: true
},
'desktop_app_content': {
cwd: 'electron/',
src: '**',
dest: 'tmp/desktop/app/',
expand: true,
nonull: true
},
'desktop_osx': {
src: 'tmp/desktop/KeeWeb.dmg',
dest: 'dist/desktop/KeeWeb.mac.dmg',
@ -141,12 +151,16 @@ module.exports = function(grunt) {
'string-replace': {
manifest: {
options: {
replacements: [{
pattern: '# YYYY-MM-DD:v0.0.0',
replacement: '# ' + dt + ':v' + pkg.version
}]
replacements: [
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version },
{ pattern: 'vElectron', replacement: electronVersion }
]
},
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
},
'desktop_html': {
options: { replacements: [{ pattern: ' manifest="manifest.appcache"', replacement: '' }] },
files: { 'tmp/desktop/app/index.html': 'dist/index.html' }
}
},
webpack: {
@ -190,7 +204,8 @@ module.exports = function(grunt) {
}]})},
{ test: /runtime\-info\.js$/, loader: StringReplacePlugin.replace({ replacements: [
{ pattern: /@@VERSION/g, replacement: function() { return pkg.version; } },
{ pattern: /@@DATE/g, replacement: function() { return dt; } }
{ pattern: /@@DATE/g, replacement: function() { return dt; } },
{ pattern: /@@COMMIT/g, replacement: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); } }
]})},
{ test: /zepto(\.min)?\.js$/, loader: 'exports?Zepto; delete window.$; delete window.Zepto;' },
{ test: /baron(\.min)?\.js$/, loader: 'exports?baron; delete window.baron;' },
@ -251,7 +266,7 @@ module.exports = function(grunt) {
electron: {
options: {
name: 'KeeWeb',
dir: 'electron',
dir: 'tmp/desktop/app',
out: 'tmp/desktop',
version: electronVersion,
overwrite: true,
@ -326,15 +341,26 @@ module.exports = function(grunt) {
},
compress: {
linux: {
options: {
archive: 'tmp/desktop/KeeWeb.linux.x64.zip'
},
options: { archive: 'tmp/desktop/KeeWeb.linux.x64.zip' },
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true }]
},
'desktop_update': {
options: { archive: 'dist/desktop/UpdateDesktop.zip' },
files: [{ cwd: 'tmp/desktop/app', src: '**', expand: true }]
}
},
'validate-desktop-update': {
desktop: {
options: {
file: 'dist/desktop/UpdateDesktop.zip',
expected: ['main.js', 'app.js', 'index.html', 'package.json', 'node_modules/node-stream-zip/node_stream_zip.js']
}
}
}
});
grunt.registerTask('default', [
'gitinfo',
'bower-install-simple',
'clean',
'jshint',
@ -347,13 +373,18 @@ module.exports = function(grunt) {
'postcss',
'inline',
'htmlmin',
'string-replace'
'string-replace:manifest'
]);
grunt.registerTask('desktop', [
'default',
'gitinfo',
'clean:desktop_tmp',
'clean:desktop_dist',
'copy:desktop_app_content',
'string-replace:desktop_html',
'compress:desktop_update',
'validate-desktop-update',
'electron',
'electron_builder',
'compress:linux',

View File

@ -24,7 +24,6 @@ Reading and display is mostly complete; modification and sync is under construct
These major issues are in progress, or will be fixed in next releases, before v1.0:
- auto-update is not implemented
- dropbox sync is one-way: changes are not loaded from dropbox, only saved
# Self-hosting
@ -34,10 +33,20 @@ You can download the latest distribution files from [gh-pages](https://github.co
# Building
The app can be built with grunt: `grunt` (html file will be in `dist/`) or `grunt watch` (result will be in `tmp/`).
Electron app is built with `grunt electron` (works only under mac osx as it builds dmg; requires wine).
To run Electron app without building, install electron package (`npm install electron-prebuilt -g`) and start with `electron ./electron/`.
For debug build: 1. run `grunt`, 2. run `grunt watch`, 3. open `tmp/index.html`.
The app can be built with grunt: `grunt` (html file will be in `dist/`).
Desktop apps are built with `grunt desktop`. This works only in mac osx as it builds dmg; requires wine.
To run Electron app without building, install electron package (`npm install electron-prebuilt -g`) and start in this way:
```bash
$ cd electron
$ electron . --htmlpath=../tmp
```
For debug build:
1. run `grunt`
2. run `grunt watch`
3. open `tmp/index.html`
# Contributing

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# YYYY-MM-DD:v0.0.0
# YYYY-MM-DD:v0.0.0 evElectron
CACHE:
index.html

View File

@ -5,6 +5,7 @@ var AppModel = require('./models/app-model'),
KeyHandler = require('./comp/key-handler'),
Alerts = require('./comp/alerts'),
DropboxLink = require('./comp/dropbox-link'),
Updater = require('./comp/updater'),
LastOpenFiles = require('./comp/last-open-files'),
ThemeChanger = require('./util/theme-changer');
@ -47,6 +48,6 @@ $(function() {
} else {
appView.showOpenFile();
}
Updater.init();
}
});

View File

@ -4,6 +4,7 @@ var Backbone = require('backbone');
var Launcher;
if (window.process && window.process.versions && window.process.versions.electron) {
/* jshint node:true */
Launcher = {
name: 'electron',
version: window.process.versions.electron,
@ -18,6 +19,9 @@ 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');
@ -32,6 +36,9 @@ if (window.process && window.process.versions && window.process.versions.electro
getUserDataPath: function(fileName) {
return this.req('path').join(this.remReq('app').getPath('userData'), fileName || '');
},
getTempPath: function(fileName) {
return this.req('path').join(this.remReq('app').getPath('temp'), fileName || '');
},
writeFile: function(path, data) {
this.req('fs').writeFileSync(path, new window.Buffer(data));
},
@ -42,9 +49,28 @@ if (window.process && window.process.versions && window.process.versions.electro
fileExists: function(path) {
return this.req('fs').existsSync(path);
},
preventExit: function(e) {
e.returnValue = false;
return false;
},
exit: function() {
Launcher.exitRequested = true;
this.remReq('app').quit();
this.requestExit();
},
requestExit: function() {
var app = this.remReq('app');
if (this.restartPending) {
app.quitAndRestart();
} else {
app.quit();
}
},
requestRestart: function() {
this.restartPending = true;
this.requestExit();
},
cancelRestart: function() {
this.restartPending = false;
}
};
window.launcherOpen = function(path) {

View File

@ -5,6 +5,7 @@ var Launcher = require('../comp/launcher');
var RuntimeInfo = {
version: '@@VERSION',
buildDate: '@@DATE',
commit: '@@COMMIT',
userAgent: navigator.userAgent,
launcher: Launcher ? Launcher.name + ' v' + Launcher.version : ''
};

View File

@ -0,0 +1,62 @@
'use strict';
var Launcher = require('./launcher');
var Transport = {
httpGet: function(config) {
var tmpFile;
var fs = Launcher.req('fs');
if (config.file) {
tmpFile = Launcher.getTempPath(config.file);
if (fs.existsSync(tmpFile)) {
try {
if (config.cache && fs.statSync(tmpFile).size > 0) {
console.log('File already downloaded ' + config.url);
return config.success(tmpFile);
} else {
fs.unlinkSync(tmpFile);
}
} catch (e) {
fs.unlink(tmpFile);
}
}
}
var proto = config.url.split(':')[0];
console.log('GET ' + config.url);
var opts = Launcher.req('url').parse(config.url);
opts.headers = { 'User-Agent': navigator.userAgent };
Launcher.req(proto).get(opts, function(res) {
console.log('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 {
config.error('HTTP status ' + res.statusCode);
}
}).on('error', function(e) {
console.error('Cannot GET ' + config.url, e);
if (tmpFile) {
fs.unlink(tmpFile);
}
config.error(e);
});
}
};
module.exports = Transport;

198
app/scripts/comp/updater.js Normal file
View File

@ -0,0 +1,198 @@
'use strict';
var Backbone = require('backbone'),
RuntimeInfo = require('./runtime-info'),
Links = require('../const/links'),
Launcher = require('../comp/launcher'),
AppSettingsModel = require('../models/app-settings-model'),
UpdateModel = require('../models/update-model'),
Transport = require('../comp/transport');
var Updater = {
UpdateInterval: 1000*60*60*24,
MinUpdateTimeout: 500,
MinUpdateSize: 10000,
UpdateCheckFiles: ['index.html', 'app.js'],
nextCheckTimeout: null,
updateCheckDate: new Date(0),
enabledAutoUpdate: function() {
return Launcher && AppSettingsModel.instance.get('autoUpdate');
},
updateInProgress: function() {
return UpdateModel.instance.get('status') === 'checking' ||
['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0;
},
init: function() {
var willCheckNow = this.scheduleNextCheck();
if (!willCheckNow && this.enabledAutoUpdate()) {
this.check();
}
if (!Launcher && window.applicationCache) {
window.applicationCache.addEventListener('updateready', this.checkAppCacheUpdateReady.bind(this));
this.checkAppCacheUpdateReady();
}
},
scheduleNextCheck: function() {
if (this.nextCheckTimeout) {
clearTimeout(this.nextCheckTimeout);
this.nextCheckTimeout = null;
}
if (!this.enabledAutoUpdate()) {
return;
}
var timeDiff = this.MinUpdateTimeout;
var lastCheckDate = UpdateModel.instance.get('lastCheckDate');
if (lastCheckDate) {
timeDiff = Math.min(Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout), this.UpdateInterval);
}
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
console.log('Update check will happen in ' + Math.round(timeDiff / 1000) + 's');
return timeDiff === this.MinUpdateTimeout;
},
check: function(startedByUser) {
if (!Launcher || this.updateInProgress()) {
return;
}
if (this.checkManualDownload()) {
return;
}
UpdateModel.instance.set('status', 'checking');
var that = this;
if (!startedByUser) {
// additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
var diffMs = new Date() - this.updateCheckDate;
if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
console.error('Prevented update check; last check was performed at ' + this.updateCheckDate);
that.scheduleNextCheck();
return;
}
this.updateCheckDate = new Date();
}
console.log('Checking for update...');
Transport.httpGet({
url: Links.WebApp + 'manifest.appcache',
utf8: true,
success: function(data) {
var dt = new Date();
var match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
console.log('Update check: ' + (match ? match[0] : 'unknown'));
if (!match) {
var errMsg = 'No version info found';
UpdateModel.instance.set({ status: 'error', lastCheckDate: dt, lastCheckError: errMsg });
UpdateModel.instance.save();
that.scheduleNextCheck();
return;
}
var prevLastVersion = UpdateModel.instance.get('lastVersion');
UpdateModel.instance.set({
status: 'ok',
lastCheckDate: dt,
lastSuccessCheckDate: dt,
lastVersionReleaseDate: new Date(match[1]),
lastVersion: match[2],
lastcheckError: null
});
UpdateModel.instance.save();
that.scheduleNextCheck();
if (prevLastVersion === UpdateModel.instance.get('lastVersion') &&
UpdateModel.instance.get('updateStatus') === 'ready') {
console.log('Waiting for the user to apply downloaded update');
return;
}
that.update(startedByUser);
},
error: function(e) {
console.error('Update check error', e);
UpdateModel.instance.set({
status: 'error',
lastCheckDate: new Date(),
lastCheckError: 'Error checking last version'
});
UpdateModel.instance.save();
that.scheduleNextCheck();
}
});
},
checkManualDownload: function() {
if (+Launcher.getAppVersion().split('.')[1] <= 2) {
UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true });
return true;
}
},
update: function(startedByUser) {
var ver = UpdateModel.instance.get('lastVersion');
if (!Launcher || ver === RuntimeInfo.version) {
console.log('You are using the latest version');
return;
}
UpdateModel.instance.set({ updateStatus: 'downloading', updateError: null });
var that = this;
console.log('Downloading update', ver);
Transport.httpGet({
url: Links.UpdateDesktop.replace('{ver}', ver),
file: 'KeeWeb-' + ver + '.zip',
cache: !startedByUser,
success: function(filePath) {
UpdateModel.instance.set('updateStatus', 'extracting');
console.log('Extracting update file', that.UpdateCheckFiles, filePath);
that.extractAppUpdate(filePath, function(err) {
if (err) {
console.error('Error extracting update', err);
UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error extracting update' });
} else {
UpdateModel.instance.set({ updateStatus: 'ready', updateError: null });
if (!startedByUser) {
Backbone.trigger('update-app');
}
}
});
},
error: function(e) {
console.error('Error downloading update', e);
UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error downloading update' });
}
});
},
extractAppUpdate: function(updateFile, cb) {
var expectedFiles = this.UpdateCheckFiles;
var appPath = Launcher.getUserDataPath();
var StreamZip = Launcher.req('node-stream-zip');
var zip = new StreamZip({ file: updateFile, storeEntries: true });
zip.on('error', cb);
zip.on('ready', function() {
var containsAll = expectedFiles.every(function(expFile) {
var entry = zip.entry(expFile);
return entry && entry.isFile;
});
if (!containsAll) {
return cb('Bad archive');
}
zip.extract(null, appPath, function(err) {
zip.close();
if (err) {
return cb(err);
}
Launcher.req('fs').unlink(updateFile);
cb();
});
});
},
checkAppCacheUpdateReady: function() {
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
try { window.applicationCache.swapCache(); } catch (e) { }
UpdateModel.instance.set('updateStatus', 'ready');
Backbone.trigger('update-app');
}
}
};
module.exports = Updater;

View File

@ -4,7 +4,9 @@ var Links = {
Repo: 'https://github.com/antelle/keeweb',
Desktop: 'https://github.com/antelle/keeweb/releases/latest',
WebApp: 'https://antelle.github.io/keeweb/',
License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt'
License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt',
UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/{ver}/UpdateDesktop.zip',
ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes'
};
module.exports = Links;

View File

@ -10,7 +10,8 @@ var AppSettingsModel = Backbone.Model.extend({
theme: 'd',
expandGroups: true,
listViewWidth: null,
menuViewWidth: null
menuViewWidth: null,
autoUpdate: true
},
initialize: function() {
@ -21,7 +22,10 @@ var AppSettingsModel = Backbone.Model.extend({
try {
var data;
if (Launcher) {
data = JSON.parse(Launcher.readFile(Launcher.getUserDataPath(FileName), 'utf8'));
var settingsFile = Launcher.getUserDataPath(FileName);
if (Launcher.fileExists(settingsFile)) {
data = JSON.parse(Launcher.readFile(settingsFile, 'utf8'));
}
} else if (typeof localStorage !== 'undefined' && localStorage.appSettings) {
data = JSON.parse(localStorage.appSettings);
}

View File

@ -0,0 +1,49 @@
'use strict';
var Backbone = require('backbone');
var UpdateModel = Backbone.Model.extend({
defaults: {
lastSuccessCheckDate: null,
lastCheckDate: null,
lastVersion: null,
lastVersionReleaseDate: null,
lastCheckError: null,
status: null,
updateStatus: null,
updateError: null,
updateManual: false
},
initialize: function() {
},
load: function() {
if (localStorage.updateInfo) {
try {
var data = JSON.parse(localStorage.updateInfo);
_.each(data, function(val, key) {
if (/Date$/.test(key)) {
data[key] = val ? new Date(val) : null;
}
});
this.set(data, { silent: true });
} catch (e) { /* failed to load model */ }
}
},
save: function() {
var attr = _.clone(this.attributes);
Object.keys(attr).forEach(function(key) {
if (key.lastIndexOf('update', 0) === 0) {
delete attr[key];
}
});
localStorage.updateInfo = JSON.stringify(attr);
}
});
UpdateModel.instance = new UpdateModel();
UpdateModel.instance.load();
module.exports = UpdateModel;

View File

@ -58,6 +58,7 @@ var AppView = Backbone.View.extend({
this.listenTo(Backbone, 'toggle-details', this.toggleDetails);
this.listenTo(Backbone, 'edit-group', this.editGroup);
this.listenTo(Backbone, 'launcher-open-file', this.launcherOpenFile);
this.listenTo(Backbone, 'update-app', this.updateApp);
window.onbeforeunload = this.beforeUnload.bind(this);
window.onresize = this.windowResize.bind(this);
@ -103,6 +104,12 @@ var AppView = Backbone.View.extend({
}
},
updateApp: function() {
if (!Launcher && !this.model.files.hasOpenFiles()) {
window.location.reload();
}
},
showEntries: function() {
this.views.menu.show();
this.views.menuDrag.show();
@ -189,16 +196,25 @@ var AppView = Backbone.View.extend({
beforeUnload: function(e) {
if (this.model.files.hasUnsavedFiles()) {
if (Launcher && !Launcher.exitRequested) {
Alerts.yesno({
header: 'Unsaved changes!',
body: 'You have unsaved files, all changes will be lost.',
buttons: [{result: 'yes', title: 'Exit and discard unsaved changes'}, {result: '', title: 'Don\'t exit'}],
success: function() {
Launcher.exit();
}
});
e.returnValue = false;
return false;
if (!this.exitAlertShown) {
var that = this;
that.exitAlertShown = true;
Alerts.yesno({
header: 'Unsaved changes!',
body: 'You have unsaved files, all changes will be lost.',
buttons: [{result: 'yes', title: 'Exit and discard unsaved changes'}, {result: '', title: 'Don\'t exit'}],
success: function () {
Launcher.exit();
},
cancel: function() {
Launcher.cancelRestart(false);
},
complete: function () {
that.exitAlertShown = false;
}
});
}
return Launcher.preventExit(e);
}
return 'You have unsaved files, all changes will be lost.';
}

View File

@ -3,7 +3,8 @@
var Backbone = require('backbone'),
Keys = require('../const/keys'),
KeyHandler = require('../comp/key-handler'),
GeneratorView = require('./generator-view');
GeneratorView = require('./generator-view'),
UpdateModel = require('../models/update-model');
var FooterView = Backbone.View.extend({
template: require('templates/footer.html'),
@ -28,10 +29,15 @@ var FooterView = Backbone.View.extend({
KeyHandler.onKey(Keys.DOM_VK_COMMA, this.toggleSettings, this, KeyHandler.SHORTCUT_ACTION);
this.listenTo(this.model.files, 'update reset change', this.render);
this.listenTo(Backbone, 'update-app', this.render);
},
render: function () {
this.$el.html(this.template(this.model));
this.listenTo(Backbone, 'update-app', this.updateApp);
this.$el.html(this.template({
files: this.model.files,
updateAvailable: UpdateModel.instance.get('updateStatus') === 'ready'
}));
return this;
},

View File

@ -2,7 +2,12 @@
var Backbone = require('backbone'),
Launcher = require('../../comp/launcher'),
AppSettingsModel = require('../../models/app-settings-model');
Updater = require('../../comp/updater'),
Format = require('../../util/format'),
AppSettingsModel = require('../../models/app-settings-model'),
UpdateModel = require('../../models/update-model'),
RuntimeInfo = require('../../comp/runtime-info'),
Links = require('../../const/links');
var SettingsGeneralView = Backbone.View.extend({
template: require('templates/settings/settings-general.html'),
@ -10,6 +15,10 @@ var SettingsGeneralView = Backbone.View.extend({
events: {
'change .settings__general-theme': 'changeTheme',
'change .settings__general-expand': 'changeExpandGroups',
'change .settings__general-auto-update': 'changeAutoUpdate',
'click .settings__general-update-btn': 'checkUpdate',
'click .settings__general-restart-btn': 'restartApp',
'click .settings__general-download-update-btn': 'downloadUpdate',
'click .settings__general-dev-tools-link': 'openDevTools'
},
@ -19,20 +28,92 @@ var SettingsGeneralView = Backbone.View.extend({
wh: 'white'
},
initialize: function() {
this.listenTo(UpdateModel.instance, 'change:status', this.render, this);
this.listenTo(UpdateModel.instance, 'change:updateStatus', this.render, this);
},
render: function() {
this.renderTemplate({
themes: this.allThemes,
activeTheme: AppSettingsModel.instance.get('theme'),
expandGroups: AppSettingsModel.instance.get('expandGroups'),
devTools: Launcher && Launcher.devTools
devTools: Launcher && Launcher.devTools,
canAutoUpdate: !!Launcher,
autoUpdate: Updater.enabledAutoUpdate(),
updateInProgress: Updater.updateInProgress(),
updateInfo: this.getUpdateInfo(),
updateReady: UpdateModel.instance.get('updateStatus') === 'ready',
updateManual: UpdateModel.instance.get('updateManual'),
releaseNotesLink: Links.ReleaseNotes
});
},
getUpdateInfo: function() {
switch (UpdateModel.instance.get('status')) {
case 'checking':
return 'Checking for updates...';
case 'error':
var errMsg = 'Error checking for updates';
if (UpdateModel.instance.get('lastError')) {
errMsg += ': ' + UpdateModel.instance.get('lastError');
}
if (UpdateModel.instance.get('lastSuccessCheckDate')) {
errMsg += '. Last successful check was at ' + Format.dtStr(UpdateModel.instance.get('lastSuccessCheckDate')) +
': the latest version was ' + UpdateModel.instance.get('lastVersion');
}
return errMsg;
case 'ok':
var msg = 'Checked at ' + Format.dtStr(UpdateModel.instance.get('lastCheckDate')) + ': ';
if (RuntimeInfo.version === UpdateModel.instance.get('lastVersion')) {
msg += 'you are using the latest version';
} else {
msg += 'new version ' + UpdateModel.instance.get('lastVersion') + ' available, released at ' +
Format.dStr(UpdateModel.instance.get('lastVersionReleaseDate'));
}
switch (UpdateModel.instance.get('updateStatus')) {
case 'downloading':
return msg + '. Downloading update...';
case 'extracting':
return msg + '. Extracting update...';
case 'error':
return msg + '. There was an error downloading new version';
}
return msg;
default:
return 'Never checked for updates';
}
},
changeTheme: function(e) {
var theme = e.target.value;
AppSettingsModel.instance.set('theme', theme);
},
changeAutoUpdate: function(e) {
var autoUpdate = e.target.checked;
AppSettingsModel.instance.set('autoUpdate', autoUpdate);
if (autoUpdate) {
Updater.scheduleNextCheck();
}
},
checkUpdate: function() {
Updater.check(true);
},
restartApp: function() {
if (Launcher) {
Launcher.requestRestart();
} else {
window.location.reload();
}
},
downloadUpdate: function() {
Launcher.openLink(Links.Desktop);
},
changeExpandGroups: function(e) {
var expand = e.target.checked;
AppSettingsModel.instance.set('expandGroups', expand);

View File

@ -8,7 +8,7 @@ var SettingsHelpView = Backbone.View.extend({
template: require('templates/settings/settings-help.html'),
render: function() {
var appInfo = 'KeeWeb v' + RuntimeInfo.version + ' (built at ' + RuntimeInfo.buildDate + ')\n' +
var appInfo = 'KeeWeb v' + RuntimeInfo.version + ' (' + RuntimeInfo.commit + ', ' + RuntimeInfo.buildDate + ')\n' +
'Environment: ' + (RuntimeInfo.launcher ? RuntimeInfo.launcher : 'web') + '\n' +
'User-Agent: ' + RuntimeInfo.userAgent;
this.renderTemplate({

View File

@ -41,4 +41,9 @@
padding: $base-padding;
font-size: 1.4em;
}
&__update-icon {
@include th { color: action-color(); }
@include animation(shake 50s cubic-bezier(.36,.07,.19,.97) 0s infinite);
}
}

View File

@ -71,4 +71,12 @@
float: right;
display: none;
}
&__general-update-buttons {
margin-top: $base-spacing;
}
&__general-update-btn {
width: 15em;
margin-right: $small-spacing;
}
}

View File

@ -57,6 +57,5 @@ $all-colors: (
@each $col, $val in $all-colors {
.#{$col}-color { color: #{$val}; }
}
.muted-color {
@include th { color: muted-color(); };
}
.muted-color { @include th { color: muted-color(); }; }
.action-color { @include th { color: action-color(); }; }

View File

@ -25,3 +25,11 @@
@include transform(rotateY(360deg));
}
}
@include keyframes(shake) {
0%, 1%, 100% { @include transform(translate3d(0, 0, 0)); }
.1%, .9% { @include transform(translate3d(-1px, 0, 0)); }
.2%, .8% { @include transform(translate3d(2px, 0, 0)); }
.3%, .5%, .7% { @include transform(translate3d(-3px, 0, 0)); }
.4%, .6% { @include transform(translate3d(3px, 0, 0)); }
}

View File

@ -9,7 +9,13 @@
<div class="footer__db footer__db--dimmed footer__db--expanded footer__db-open"><i class="fa fa-plus"></i> Open / New</div>
<!--<div class="footer__btn footer__btn-view"><i class="fa fa-list-ul"></i></div>-->
<div class="footer__btn footer__btn-help"><i class="fa fa-question"></i></div>
<div class="footer__btn footer__btn-settings"><i class="fa fa-cog"></i></div>
<div class="footer__btn footer__btn-settings">
<% if (updateAvailable) { %>
<i class="fa fa-bell footer__update-icon"></i>
<% } else { %>
<i class="fa fa-cog"></i>
<% } %>
</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>

View File

@ -1,5 +1,19 @@
<div>
<h1><i class="fa fa-cog"></i> General Settings</h1>
<% if (updateReady && !canAutoUpdate) { %>
<h2 class="action-color">Update</h2>
<div>New app version was released and downloaded. <a href="<%= releaseNotesLink %>" target="_blank">View release notes</a></div>
<div class="settings__general-update-buttons">
<button class="settings__general-restart-btn">Reload to update</button>
</div>
<% } else if (updateManual) { %>
<h2 class="action-color">Update</h2>
<div>New version has been released. It will check for updates and install them automatically
but auto-upgrading from your version is impossible.</div>
<div class="settings__general-update-buttons">
<button class="settings__general-download-update-btn">Download update</button>
</div>
<% } %>
<h2>Appearance</h2>
<div>
<label for="settings__general-theme">Theme:</label>
@ -13,6 +27,25 @@
<input type="checkbox" class="settings__input input-base settings__general-expand" id="settings__general-expand" <%- expandGroups ? 'checked' : '' %> />
<label for="settings__general-expand">Show entries from all subgroups</label>
</div>
<% if (canAutoUpdate && !updateManual) { %>
<h2>Function</h2>
<div>
<input type="checkbox" class="settings__input settings__general-auto-update" id="settings__general-auto-update" <%- autoUpdate ? 'checked' : '' %> />
<label for="settings__general-auto-update">Automatic updates</label>
<div><%- updateInfo %></div>
<a href="<%= releaseNotesLink %>" target="_blank">View release notes</a>
</div>
<div class="settings__general-update-buttons">
<% if (updateInProgress) { %>
<button class="settings__general-update-btn btn-silent" disabled>Checking for updates</button>
<% } else { %>
<button class="settings__general-update-btn btn-silent">Check for updates</button>
<% } %>
<% if (updateReady) { %>
<button class="settings__general-restart-btn">Restart to update</button>
<% } %>
</div>
<% } %>
<% if (devTools) { %>
<h2>Advanced</h2>
<a class="settings__general-dev-tools-link">Show dev tools</a>

104
electron/app.js Normal file
View File

@ -0,0 +1,104 @@
'use strict';
/* jshint node:true */
/* jshint browser:false */
var app = require('app'),
BrowserWindow = require('browser-window'),
path = require('path'),
Menu = require('menu');
var mainWindow = null,
openFile = process.argv.filter(function(arg) { return /\.kdbx$/i.test(arg); })[0],
ready = false,
restartPending = false,
htmlPath = path.join(__dirname, 'index.html');
process.argv.forEach(function(arg) {
if (arg.lastIndexOf('--htmlpath=', 0) === 0) {
htmlPath = path.resolve(arg.replace('--htmlpath=', ''), 'index.html');
}
});
app.on('window-all-closed', function() {
app.quit();
});
app.on('ready', function() {
mainWindow = new BrowserWindow({
show: false,
width: 1000, height: 700, 'min-width': 600, 'min-height': 300,
icon: path.join(__dirname, 'icon.png')
});
setMenu();
mainWindow.loadUrl('file://' + htmlPath);
mainWindow.webContents.on('dom-ready', function() {
setTimeout(function() {
mainWindow.show();
ready = true;
notifyOpenFile();
}, 50);
});
mainWindow.on('closed', function() {
mainWindow = null;
});
});
app.on('open-file', function(e, path) {
e.preventDefault();
openFile = path;
notifyOpenFile();
});
app.on('quit', function() {
if (restartPending) {
require('child_process').exec(process.execPath);
}
});
app.quitAndRestart = function() {
restartPending = true;
app.quit();
setTimeout(function() { restartPending = false; }, 1000);
};
function setMenu() {
if (process.platform === 'darwin') {
var name = require('app').getName();
var template = [
{
label: name,
submenu: [
{ label: 'About ' + name, role: 'about' },
{ type: 'separator' },
{ label: 'Services', role: 'services', submenu: [] },
{ type: 'separator' },
{ label: 'Hide ' + name, accelerator: 'Command+H', role: 'hide' },
{ label: 'Hide Others', accelerator: 'Command+Shift+H', role: 'hideothers' },
{ label: 'Show All', role: 'unhide' },
{ type: 'separator' },
{ label: 'Quit', accelerator: 'Command+Q', click: function() { app.quit(); } }
]
},
{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' }
]
}
];
var menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
}
function notifyOpenFile() {
if (ready && openFile && mainWindow) {
openFile = openFile.replace(/"/g, '\\"').replace(/\\/g, '\\\\');
mainWindow.webContents.executeJavaScript('if (window.launcherOpen) { window.launcherOpen("' + openFile + '"); } ' +
' else { window.launcherOpenedFile="' + openFile + '"; }');
openFile = null;
}
}

View File

@ -1,87 +1,43 @@
// KeeWeb launcher script
// This script is distributed with the app and is its entry point
// It checks whether the app is available in userData folder and if its version is higher than local, launches it
// This script is the only part which will be updated only with the app itself, auto-update will not change it
// (C) Antelle 2015, MIT license https://github.com/antelle/keeweb
'use strict';
/* jshint node:true */
/* jshint browser:false */
var app = require('app'),
BrowserWindow = require('browser-window'),
path = require('path'),
fs = require('fs'),
Menu = require('menu');
fs = require('fs');
var mainWindow = null,
openFile = process.argv.filter(function(arg) { return /\.kdbx$/i.test(arg); })[0],
ready = false;
var userDataDir = app.getPath('userData'),
appPathUserData = path.join(userDataDir, 'app.js'),
appPath = path.join(__dirname, 'app.js');
app.on('window-all-closed', function() { app.quit(); });
app.on('ready', function() {
var htmlPath = path.join(app.getPath('userData'), 'index.html');
mainWindow = new BrowserWindow({
show: false,
width: 1000, height: 700, 'min-width': 600, 'min-height': 300,
icon: path.join(__dirname, 'icon.png')
});
setMenu();
if (fs.existsSync(htmlPath)) {
mainWindow.loadUrl('file://' + htmlPath);
} else {
mainWindow.loadUrl('https://antelle.github.io/keeweb/index.html');
}
mainWindow.webContents.on('dom-ready', function() {
mainWindow.show();
ready = true;
notifyOpenFile();
});
mainWindow.on('closed', function() { mainWindow = null; });
});
app.on('open-file', function(e, path) {
e.preventDefault();
openFile = path;
notifyOpenFile();
});
function setMenu() {
if (process.platform === 'darwin') {
var name = require('app').getName();
var template = [
{
label: name,
submenu: [
{ label: 'About ' + name, role: 'about' },
{ type: 'separator' },
{ label: 'Services', role: 'services', submenu: [] },
{ type: 'separator' },
{ label: 'Hide ' + name, accelerator: 'Command+H', role: 'hide' },
{ label: 'Hide Others', accelerator: 'Command+Shift+H', role: 'hideothers' },
{ label: 'Show All', role: 'unhide' },
{ type: 'separator' },
{ label: 'Quit', accelerator: 'Command+Q', click: function() { app.quit(); } }
]
},
{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' }
]
if (fs.existsSync(appPathUserData)) {
var versionLocal = require('./package.json').version;
try {
var versionUserData = require(path.join(userDataDir, 'package.json')).version;
versionLocal = versionLocal.split('.');
versionUserData = versionUserData.split('.');
for (var i = 0; i < versionLocal.length; i++) {
if (+versionUserData[i] > +versionLocal[i]) {
appPath = appPathUserData;
break;
}
];
var menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
if (+versionUserData[i] < +versionLocal[i]) {
break;
}
}
}
catch (e) {
console.error('Error reading user file version', e);
}
}
function notifyOpenFile() {
if (ready && openFile && mainWindow) {
openFile = openFile.replace(/"/g, '\\"').replace(/\\/g, '\\\\');
mainWindow.webContents.executeJavaScript('if (window.launcherOpen) { window.launcherOpen("' + openFile + '"); } ' +
' else { window.launcherOpenedFile="' + openFile + '"; }');
openFile = null;
}
}
require(appPath);

View File

@ -1,5 +1,13 @@
{
"name": "KeeWeb",
"version": "0.2.1",
"main": "main.js"
"description": "KeePass web app",
"main": "main.js",
"repository": "https://github.com/antelle/keeweb",
"author": "Antelle",
"license": "MIT",
"readme": "../README.md",
"dependencies": {
"node-stream-zip": "^1.2.1"
}
}

View File

@ -0,0 +1,31 @@
'use strict';
module.exports = function (grunt) {
grunt.registerMultiTask('validate-desktop-update', 'Validates desktop update package', function () {
var path = require('path');
var done = this.async();
var StreamZip = require(path.resolve(__dirname, '../../electron/node_modules/node-stream-zip'));
var zip = new StreamZip({ file: this.options().file, storeEntries: true });
var expFiles = this.options().expected;
zip.on('error', function(err) {
grunt.warn(err);
});
zip.on('ready', function() {
var valid = true;
expFiles.forEach(function(entry) {
try {
if (!zip.entryDataSync(entry)) {
grunt.warn('Corrupted entry in desktop update archive: ' + entry);
valid = false;
}
} catch (e) {
grunt.warn('Entry not found in desktop update archive: ' + entry);
valid = false;
}
});
if (valid) {
done();
}
});
});
};

View File

@ -32,12 +32,14 @@
"time-grunt": "^1.2.1",
"uglify-loader": "^1.2.0",
"webpack": "^1.11.0",
"webpack-dev-server": "^1.10.1"
"webpack-dev-server": "^1.12.1"
},
"scripts": {
"start": "grunt",
"test": "grunt test"
"test": "grunt test",
"postinstall": "npm install --prefix electron"
},
"author": "Antelle",
"license": "MIT"
"license": "MIT",
"readme": "README.md"
}