From cbe109b59754f03057074d3e7a04e1a6a41f5269 Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 1 Dec 2015 22:28:25 +0300 Subject: [PATCH 01/49] up kdbxweb --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 0ff0ad38..82b8da3b 100644 --- a/bower.json +++ b/bower.json @@ -29,7 +29,7 @@ "dropbox": "antelle/dropbox-js#0.10.5", "font-awesome": "~4.4.0", "install": "~1.0.4", - "kdbxweb": "~0.2.7", + "kdbxweb": "~0.3.0", "normalize.css": "~3.0.3", "pikaday": "~1.3.3", "zepto": "~1.1.6", From efec113ff8cdaea5a825aa3869193aaf39834535 Mon Sep 17 00:00:00 2001 From: Antelle Date: Wed, 2 Dec 2015 21:16:33 +0300 Subject: [PATCH 02/49] up kdbxweb --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 82b8da3b..f2a007fb 100644 --- a/bower.json +++ b/bower.json @@ -29,7 +29,7 @@ "dropbox": "antelle/dropbox-js#0.10.5", "font-awesome": "~4.4.0", "install": "~1.0.4", - "kdbxweb": "~0.3.0", + "kdbxweb": "~0.3.1", "normalize.css": "~3.0.3", "pikaday": "~1.3.3", "zepto": "~1.1.6", From 6779044f13f03e6e33752823e5419cbfda013a0d Mon Sep 17 00:00:00 2001 From: Antelle Date: Wed, 2 Dec 2015 23:39:40 +0300 Subject: [PATCH 03/49] storage providers refactoring --- app/scripts/comp/last-open-files.js | 2 +- app/scripts/comp/storage/index.js | 13 ------- app/scripts/comp/storage/storage-dropbox.js | 11 ------ app/scripts/comp/storage/storage-file.js | 11 ------ app/scripts/models/file-model.js | 36 ++++++++----------- app/scripts/storage/index.js | 9 +++++ .../{comp => }/storage/storage-cache.js | 0 app/scripts/storage/storage-dropbox.js | 18 ++++++++++ app/scripts/storage/storage-file.js | 30 ++++++++++++++++ app/scripts/views/open-view.js | 26 +++++++------- .../views/settings/settings-file-view.js | 24 +++++++------ 11 files changed, 98 insertions(+), 82 deletions(-) delete mode 100644 app/scripts/comp/storage/index.js delete mode 100644 app/scripts/comp/storage/storage-dropbox.js delete mode 100644 app/scripts/comp/storage/storage-file.js create mode 100644 app/scripts/storage/index.js rename app/scripts/{comp => }/storage/storage-cache.js (100%) create mode 100644 app/scripts/storage/storage-dropbox.js create mode 100644 app/scripts/storage/storage-file.js diff --git a/app/scripts/comp/last-open-files.js b/app/scripts/comp/last-open-files.js index 2fefac4e..304e52ec 100644 --- a/app/scripts/comp/last-open-files.js +++ b/app/scripts/comp/last-open-files.js @@ -1,6 +1,6 @@ 'use strict'; -var Storage = require('./storage'); +var Storage = require('../storage'); var MaxItems = 5; diff --git a/app/scripts/comp/storage/index.js b/app/scripts/comp/storage/index.js deleted file mode 100644 index 4dfa0c7c..00000000 --- a/app/scripts/comp/storage/index.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -var Storage = {}; - -[ - require('./storage-cache'), - require('./storage-dropbox'), - require('./storage-file') -].forEach(function(storage) { - Storage[storage.name] = storage; -}); - -module.exports = Storage; diff --git a/app/scripts/comp/storage/storage-dropbox.js b/app/scripts/comp/storage/storage-dropbox.js deleted file mode 100644 index 8f9d4950..00000000 --- a/app/scripts/comp/storage/storage-dropbox.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -var Launcher = require('../launcher'); - -var StorageDropbox = { - name: 'dropbox', - enabled: !Launcher - // TODO: move Dropbox storage operations here -}; - -module.exports = StorageDropbox; diff --git a/app/scripts/comp/storage/storage-file.js b/app/scripts/comp/storage/storage-file.js deleted file mode 100644 index b14eb847..00000000 --- a/app/scripts/comp/storage/storage-file.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -var Launcher = require('../launcher'); - -var StorageFile = { - name: 'file', - enabled: !!Launcher - // TODO: move file storage operations here -}; - -module.exports = StorageFile; diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 8809af0b..36aa6840 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -5,7 +5,7 @@ var Backbone = require('backbone'), GroupModel = require('./group-model'), Launcher = require('../comp/launcher'), DropboxLink = require('../comp/dropbox-link'), - Storage = require('../comp/storage'), + Storage = require('../storage'), LastOpenFiles = require('../comp/last-open-files'), IconUrl = require('../util/icon-url'), kdbxweb = require('kdbxweb'), @@ -230,28 +230,20 @@ var FileModel = Backbone.Model.extend({ autoSave: function(complete) { var that = this; that.set('syncing', true); - switch (that.get('storage')) { - case 'file': - that.getData(function(data) { - Launcher.writeFile(that.get('path'), data); - that.saved(that.get('path'), that.get('storage')); - if (complete) { complete(); } + var storage = Storage[that.get('storage')]; + if (storage) { + that.getData(function(data) { + storage.save(that.get('path'), data, function (err) { + if (err) { + that.set('syncing', false); + } else { + that.saved(that.get('path'), that.get('storage')); + } + if (complete) { complete(err); } }); - break; - case 'dropbox': - that.getData(function(data) { - DropboxLink.saveFile(that.get('path'), data, true, function (err) { - if (err) { - that.set('syncing', false); - } else { - that.saved(that.get('path'), that.get('storage')); - } - if (complete) { complete(err); } - }); - }); - break; - default: - throw 'Unknown storage; cannot auto save'; + }); + } else { + throw 'Unknown storage; cannot auto save'; } }, diff --git a/app/scripts/storage/index.js b/app/scripts/storage/index.js new file mode 100644 index 00000000..c698a550 --- /dev/null +++ b/app/scripts/storage/index.js @@ -0,0 +1,9 @@ +'use strict'; + +var Storage = { + file: require('./storage-file'), + dropbox: require('./storage-dropbox'), + cache: require('./storage-cache') +}; + +module.exports = Storage; diff --git a/app/scripts/comp/storage/storage-cache.js b/app/scripts/storage/storage-cache.js similarity index 100% rename from app/scripts/comp/storage/storage-cache.js rename to app/scripts/storage/storage-cache.js diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js new file mode 100644 index 00000000..8c88c8c6 --- /dev/null +++ b/app/scripts/storage/storage-dropbox.js @@ -0,0 +1,18 @@ +'use strict'; + +var DropboxLink = require('../comp/dropbox-link'); + +var StorageDropbox = { + name: 'dropbox', + enabled: true, + + load: function(path, callback) { + DropboxLink.openFile(path, callback); + }, + + save: function(path, data, callback) { + DropboxLink.saveFile(path, data, true, callback); + } +}; + +module.exports = StorageDropbox; diff --git a/app/scripts/storage/storage-file.js b/app/scripts/storage/storage-file.js new file mode 100644 index 00000000..841f2327 --- /dev/null +++ b/app/scripts/storage/storage-file.js @@ -0,0 +1,30 @@ +'use strict'; + +var Launcher = require('../comp/launcher'); + +var StorageFile = { + name: 'file', + enabled: !!Launcher, + + load: function(path, callback) { + try { + var data = Launcher.readFile(path); + callback(data.buffer); + } catch (e) { + console.error('Error reading local file', path, e); + callback(null, e); + } + }, + + save: function(path, data, callback) { + try { + Launcher.writeFile(path, data); + callback(); + } catch (e) { + console.error('Error writing local file', path, e); + callback(e); + } + } +}; + +module.exports = StorageFile; diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index 44e88b2f..f3ae35c0 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -7,7 +7,7 @@ var Backbone = require('backbone'), FileModel = require('../models/file-model'), Launcher = require('../comp/launcher'), LastOpenFiles = require('../comp/last-open-files'), - Storage = require('../comp/storage'), + Storage = require('../storage'), DropboxLink = require('../comp/dropbox-link'); var OpenView = Backbone.View.extend({ @@ -191,18 +191,18 @@ var OpenView = Backbone.View.extend({ showOpenLocalFile: function(path) { if (path && Launcher) { - try { - var name = path.match(/[^/\\]*$/)[0]; - var data = Launcher.readFile(path); - var file = new Blob([data]); - Object.defineProperties(file, { - path: { value: path }, - name: { value: name } - }); - this.setFile(file); - } catch (e) { - console.log('Failed to show local file', e); - } + var that = this; + Storage.file.load(path, function(data, err) { + if (!err) { + var name = path.match(/[^/\\]*$/)[0]; + var file = new Blob([data]); + Object.defineProperties(file, { + path: { value: path }, + name: { value: name } + }); + that.setFile(file); + } + }); } }, diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index 1edc6bdb..bb33cbeb 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -5,6 +5,7 @@ var Backbone = require('backbone'), PasswordGenerator = require('../../util/password-generator'), Alerts = require('../../comp/alerts'), Launcher = require('../../comp/launcher'), + Storage = require('../../storage'), Links = require('../../const/links'), DropboxLink = require('../../comp/dropbox-link'), kdbxweb = require('kdbxweb'), @@ -122,17 +123,18 @@ var SettingsAboutView = Backbone.View.extend({ }, saveToFileWithPath: function(path, data) { - try { - Launcher.writeFile(path, data); - this.passwordChanged = false; - this.model.saved(path, 'file'); - } catch (e) { - console.error('Error saving file', path, e); - Alerts.error({ - header: 'Save error', - body: 'Error saving to file ' + path + ': \n' + e - }); - } + var that = this; + Storage.file.save(path, data, function(err) { + if (err) { + Alerts.error({ + header: 'Save error', + body: 'Error saving to file ' + path + ': \n' + e + }); + } else { + that.passwordChanged = false; + that.model.saved(path, 'file'); + } + }); }, exportAsXml: function() { From e6f742921307a070e081077a7b9d1d4b1cd0698f Mon Sep 17 00:00:00 2001 From: Antelle Date: Wed, 2 Dec 2015 23:50:31 +0300 Subject: [PATCH 04/49] disallow opening same files twice --- app/scripts/collections/file-collection.js | 4 ++++ app/scripts/models/app-model.js | 8 +++++++- app/scripts/models/file-model.js | 4 +++- app/scripts/views/open-view.js | 4 +++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/scripts/collections/file-collection.js b/app/scripts/collections/file-collection.js index e12bc4fb..db60fa57 100644 --- a/app/scripts/collections/file-collection.js +++ b/app/scripts/collections/file-collection.js @@ -16,6 +16,10 @@ var FileCollection = Backbone.Collection.extend({ getByName: function(name) { return this.find(function(file) { return file.get('name') === name; }); + }, + + getById: function(id) { + return this.find(function(file) { return file.get('id') === id; }); } }); diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index ebf8144f..e73e13c3 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -28,8 +28,13 @@ var AppModel = Backbone.Model.extend({ }, addFile: function(file) { + if (this.files.getById(file.get('id'))) { + return false; + } this.files.add(file); - file.get('groups').forEach(function(group) { this.menu.groupsSection.addItem(group); }, this); + file.get('groups').forEach(function (group) { + this.menu.groupsSection.addItem(group); + }, this); this._addTags(file.db); this._tagsChanged(); this.menu.filesSection.addItem({ @@ -39,6 +44,7 @@ var AppModel = Backbone.Model.extend({ file: file }); this.refresh(); + return true; }, _addTags: function(group) { diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 36aa6840..6e806a28 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -13,6 +13,7 @@ var Backbone = require('backbone'), var FileModel = Backbone.Model.extend({ defaults: { + id: '', name: '', keyFileName: '', passwordLength: 0, @@ -122,7 +123,7 @@ var FileModel = Backbone.Model.extend({ kdbxweb.Kdbx.load(demoFile, credentials, (function(db) { this.db = db; this.readModel(); - this.setOpenFile({passwordLength: 4, demo: true, name: 'Demo'}); + this.setOpenFile({passwordLength: 4, demo: true, name: 'Demo' }); }).bind(this)); }, @@ -145,6 +146,7 @@ var FileModel = Backbone.Model.extend({ readModel: function(topGroupTitle) { var groups = new GroupCollection(); this.set({ + id: this.db.getDefaultGroup().uuid.toString(), groups: groups, defaultUser: this.db.meta.defaultUser, recycleBinEnabled: this.db.meta.recycleBinEnabled, diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index f3ae35c0..c17f3d1b 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -80,7 +80,9 @@ var OpenView = Backbone.View.extend({ }, fileOpenChanged: function() { - this.model.addFile(this.file); + if (!this.model.addFile(this.file)) { + this.trigger('cancel'); + } }, fileOpeningChanged: function() { From 99baca4b45fbcca22efe30cf62590afd739e6caa Mon Sep 17 00:00:00 2001 From: Antelle Date: Thu, 3 Dec 2015 00:12:14 +0300 Subject: [PATCH 05/49] fix #46: option to show colorful icons --- app/scripts/models/app-settings-model.js | 3 ++- app/scripts/presenters/entry-presenter.js | 5 +++-- app/scripts/views/list-view.js | 3 ++- app/scripts/views/settings/settings-general-view.js | 10 +++++++++- app/styles/areas/_list.scss | 1 - app/templates/settings/settings-general.html | 4 ++++ release-notes.md | 5 +++++ 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/scripts/models/app-settings-model.js b/app/scripts/models/app-settings-model.js index 22309946..9e3ff2c4 100644 --- a/app/scripts/models/app-settings-model.js +++ b/app/scripts/models/app-settings-model.js @@ -17,7 +17,8 @@ var AppSettingsModel = Backbone.Model.extend({ autoSave: true, idleMinutes: 15, minimizeOnClose: false, - tableView: false + tableView: false, + colorfulIcons: false }, initialize: function() { diff --git a/app/scripts/presenters/entry-presenter.js b/app/scripts/presenters/entry-presenter.js index 1af2bc8e..6f272d87 100644 --- a/app/scripts/presenters/entry-presenter.js +++ b/app/scripts/presenters/entry-presenter.js @@ -2,9 +2,10 @@ var Format = require('../util/format'); -var EntryPresenter = function(descField) { +var EntryPresenter = function(descField, noColor) { this.entry = null; this.descField = descField; + this.noColor = noColor || ''; }; EntryPresenter.prototype = { @@ -19,7 +20,7 @@ EntryPresenter.prototype = { get id() { return this.entry ? this.entry.id : this.group.get('id'); }, get icon() { return this.entry ? this.entry.icon : (this.group.get('icon') || 'folder'); }, get customIcon() { return this.entry ? this.entry.customIcon : undefined; }, - get color() { return this.entry ? this.entry.color : undefined; }, + get color() { return this.entry ? (this.entry.color || (this.entry.customIcon ? this.noColor : undefined)) : undefined; }, get title() { return this.entry ? this.entry.title : this.group.get('title'); }, get notes() { return this.entry ? this.entry.notes : undefined; }, get url() { return this.entry ? this.entry.url : undefined; }, diff --git a/app/scripts/views/list-view.js b/app/scripts/views/list-view.js index 6c906380..0f4e297b 100644 --- a/app/scripts/views/list-view.js +++ b/app/scripts/views/list-view.js @@ -66,7 +66,8 @@ var ListView = Backbone.View.extend({ if (this.items.length) { var itemTemplate = this.getItemTemplate(); var itemsTemplate = this.getItemsTemplate(); - var presenter = new EntryPresenter(this.getDescField()); + var noColor = AppSettingsModel.instance.get('colorfulIcons') ? '' : 'grayscale'; + var presenter = new EntryPresenter(this.getDescField(), noColor); var itemsHtml = ''; this.items.forEach(function (item) { presenter.present(item); diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index 60b07fb7..6e170d8d 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -22,6 +22,7 @@ var SettingsGeneralView = Backbone.View.extend({ 'change .settings__general-auto-save': 'changeAutoSave', 'change .settings__general-minimize': 'changeMinimize', 'change .settings__general-table-view': 'changeTableView', + 'change .settings__general-colorful-icons': 'changeColorfulIcons', 'click .settings__general-update-btn': 'checkUpdate', 'click .settings__general-restart-btn': 'restartApp', 'click .settings__general-download-update-btn': 'downloadUpdate', @@ -61,7 +62,8 @@ var SettingsGeneralView = Backbone.View.extend({ updateReady: UpdateModel.instance.get('updateStatus') === 'ready', updateFound: UpdateModel.instance.get('updateStatus') === 'found', updateManual: UpdateModel.instance.get('updateManual'), - releaseNotesLink: Links.ReleaseNotes + releaseNotesLink: Links.ReleaseNotes, + colorfulIcons: AppSettingsModel.instance.get('colorfulIcons') }); }, @@ -144,6 +146,12 @@ var SettingsGeneralView = Backbone.View.extend({ Backbone.trigger('refresh'); }, + changeColorfulIcons: function(e) { + var colorfulIcons = e.target.checked || false; + AppSettingsModel.instance.set('colorfulIcons', colorfulIcons); + Backbone.trigger('refresh'); + }, + restartApp: function() { if (Launcher) { Launcher.requestRestart(); diff --git a/app/styles/areas/_list.scss b/app/styles/areas/_list.scss index 1a3b66bc..e13c73af 100644 --- a/app/styles/areas/_list.scss +++ b/app/styles/areas/_list.scss @@ -104,7 +104,6 @@ height: 14px; &--custom { vertical-align: text-bottom; - @include filter(grayscale(1)); &.yellow { @include filter(grayscale(1) sepia(1) hue-rotate(20deg) brightness(1.17) saturate(5.7)); } &.green { @include filter(grayscale(1) sepia(1) hue-rotate(55deg) brightness(1.01) saturate(4.9)); } &.red { @include filter(grayscale(1) sepia(1) hue-rotate(316deg) brightness(1.1) saturate(6)); } diff --git a/app/templates/settings/settings-general.html b/app/templates/settings/settings-general.html index e0c875f3..ef977c67 100644 --- a/app/templates/settings/settings-general.html +++ b/app/templates/settings/settings-general.html @@ -56,6 +56,10 @@ <% } %> +
+ /> + +

Function

diff --git a/release-notes.md b/release-notes.md index d8f445a4..0a9fc5db 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,10 @@ Release notes ------------- +##### v0.5 (not released yet) +2-way sync +`*` disallow opening same files twice +`+` #46: option to show colorful icons + ##### v0.4.6 (2015-11-25) `-` #32: visual glitches on Windows 10 From a8bdcb203842945fa7275c75986ba7da7bdaa1c0 Mon Sep 17 00:00:00 2001 From: Antelle Date: Thu, 3 Dec 2015 00:41:53 +0300 Subject: [PATCH 06/49] fix #45: optional auto-lock on minimize --- app/scripts/comp/launcher.js | 3 + app/scripts/models/app-settings-model.js | 3 +- app/scripts/models/file-model.js | 2 - app/scripts/views/app-view.js | 8 ++- .../views/settings/settings-file-view.js | 2 +- .../views/settings/settings-general-view.js | 9 ++- app/templates/settings/settings-general.html | 9 ++- electron/app.js | 59 +++++++++++++------ release-notes.md | 1 + 9 files changed, 69 insertions(+), 27 deletions(-) diff --git a/app/scripts/comp/launcher.js b/app/scripts/comp/launcher.js index b931fe82..bb3ce0c8 100644 --- a/app/scripts/comp/launcher.js +++ b/app/scripts/comp/launcher.js @@ -88,6 +88,9 @@ if (window.process && window.process.versions && window.process.versions.electro Backbone.on('launcher-exit-request', function() { setTimeout(function() { Launcher.exit(); }, 0); }); + Backbone.on('launcher-minimize', function() { + setTimeout(function() { Backbone.trigger('app-minimized'); }, 0); + }); window.launcherOpen = function(path) { Backbone.trigger('launcher-open-file', path); }; diff --git a/app/scripts/models/app-settings-model.js b/app/scripts/models/app-settings-model.js index 9e3ff2c4..96671a12 100644 --- a/app/scripts/models/app-settings-model.js +++ b/app/scripts/models/app-settings-model.js @@ -18,7 +18,8 @@ var AppSettingsModel = Backbone.Model.extend({ idleMinutes: 15, minimizeOnClose: false, tableView: false, - colorfulIcons: false + colorfulIcons: false, + lockOnMinimize: true }, initialize: function() { diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 6e806a28..5a4c216c 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -3,8 +3,6 @@ var Backbone = require('backbone'), GroupCollection = require('../collections/group-collection'), GroupModel = require('./group-model'), - Launcher = require('../comp/launcher'), - DropboxLink = require('../comp/dropbox-link'), Storage = require('../storage'), LastOpenFiles = require('../comp/last-open-files'), IconUrl = require('../util/icon-url'), diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index 3ef76fdf..0fdf70dc 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -64,6 +64,7 @@ var AppView = Backbone.View.extend({ this.listenTo(Backbone, 'edit-group', this.editGroup); this.listenTo(Backbone, 'launcher-open-file', this.launcherOpenFile); this.listenTo(Backbone, 'user-idle', this.userIdle); + this.listenTo(Backbone, 'app-minimized', this.appMinimized); this.listenTo(UpdateModel.instance, 'change:updateReady', this.updateApp); @@ -232,7 +233,6 @@ var AppView = Backbone.View.extend({ return 'You have unsaved files, all changes will be lost.'; } else if (Launcher && !Launcher.exitRequested && !Launcher.restartPending && Launcher.canMinimize() && this.model.settings.get('minimizeOnClose')) { - this.lockWorkspace(true); Launcher.minimizeApp(); return Launcher.preventExit(e); } @@ -269,6 +269,12 @@ var AppView = Backbone.View.extend({ this.lockWorkspace(true); }, + appMinimized: function() { + if (this.model.settings.get('lockOnMinimize')) { + this.lockWorkspace(true); + } + }, + lockWorkspace: function(autoInit) { var that = this; if (Alerts.alertDisplayed) { diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index bb33cbeb..9090f2bb 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -128,7 +128,7 @@ var SettingsAboutView = Backbone.View.extend({ if (err) { Alerts.error({ header: 'Save error', - body: 'Error saving to file ' + path + ': \n' + e + body: 'Error saving to file ' + path + ': \n' + err }); } else { that.passwordChanged = false; diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index 6e170d8d..a3fad74d 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -21,6 +21,7 @@ var SettingsGeneralView = Backbone.View.extend({ 'change .settings__general-clipboard': 'changeClipboard', 'change .settings__general-auto-save': 'changeAutoSave', 'change .settings__general-minimize': 'changeMinimize', + 'change .settings__general-lock-on-minimize': 'changeLockOnMinimize', 'change .settings__general-table-view': 'changeTableView', 'change .settings__general-colorful-icons': 'changeColorfulIcons', 'click .settings__general-update-btn': 'checkUpdate', @@ -53,7 +54,8 @@ var SettingsGeneralView = Backbone.View.extend({ minimizeOnClose: AppSettingsModel.instance.get('minimizeOnClose'), devTools: Launcher && Launcher.devTools, canAutoUpdate: !!Launcher, - canMinimizeOnClose: Launcher && Launcher.canMinimize(), + canMinimize: Launcher && Launcher.canMinimize(), + lockOnMinimize: Launcher && AppSettingsModel.instance.get('lockOnMinimize'), tableView: AppSettingsModel.instance.get('tableView'), canSetTableView: FeatureDetector.isDesktop(), autoUpdate: Updater.getAutoUpdateType(), @@ -140,6 +142,11 @@ var SettingsGeneralView = Backbone.View.extend({ AppSettingsModel.instance.set('minimizeOnClose', minimizeOnClose); }, + changeLockOnMinimize: function(e) { + var lockOnMinimize = e.target.checked || false; + AppSettingsModel.instance.set('lockOnMinimize', lockOnMinimize); + }, + changeTableView: function(e) { var tableView = e.target.checked || false; AppSettingsModel.instance.set('tableView', tableView); diff --git a/app/templates/settings/settings-general.html b/app/templates/settings/settings-general.html index ef977c67..2f3bfd3d 100644 --- a/app/templates/settings/settings-general.html +++ b/app/templates/settings/settings-general.html @@ -89,13 +89,18 @@
<% } %> - <% if (canMinimizeOnClose) { %> + <% if (canMinimize) { %>
/> + <%- minimizeOnClose ? 'checked' : '' %> />
<% } %> +
+ /> + +
<% if (devTools) { %>

Advanced

diff --git a/electron/app.js b/electron/app.js index 9bf72d43..1b4fd90a 100644 --- a/electron/app.js +++ b/electron/app.js @@ -28,38 +28,32 @@ app.on('window-all-closed', function() { app.removeAllListeners('window-all-closed'); app.removeAllListeners('ready'); app.removeAllListeners('open-file'); + app.removeAllListeners('activate'); var userDataAppFile = path.join(app.getPath('userData'), 'app.js'); delete require.cache[require.resolve('./app.js')]; require(userDataAppFile); app.emit('ready'); } else { - app.quit(); + if (process.platform !== 'darwin') { + 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; - }); + createMainWindow(); }); app.on('open-file', function(e, path) { e.preventDefault(); openFile = path; notifyOpenFile(); }); +app.on('activate', function() { + if (process.platform === 'darwin') { + if (!mainWindow) { + createMainWindow(); + } + } +}); app.restartApp = function() { restartPending = true; mainWindow.close(); @@ -83,6 +77,29 @@ app.minimizeApp = function() { } }; +function createMainWindow() { + 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; + }); + mainWindow.on('minimize', function() { + emitBackboneEvent('launcher-minimize'); + }); +} + function restoreMainWindow() { appIcon.destroy(); appIcon = null; @@ -93,7 +110,11 @@ function restoreMainWindow() { function closeMainWindow() { appIcon.destroy(); appIcon = null; - mainWindow.webContents.executeJavaScript('Backbone.trigger("launcher-exit-request");'); + emitBackboneEvent('launcher-exit-request'); +} + +function emitBackboneEvent(e) { + mainWindow.webContents.executeJavaScript('Backbone.trigger("' + e + '");'); } function setMenu() { diff --git a/release-notes.md b/release-notes.md index 0a9fc5db..3ad9f749 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ Release notes 2-way sync `*` disallow opening same files twice `+` #46: option to show colorful icons +`+` #45: optional auto-lock on minimize ##### v0.4.6 (2015-11-25) `-` #32: visual glitches on Windows 10 From ab6f14f72bf61ea770474af3f241551e96d42cc8 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 4 Dec 2015 23:22:07 +0300 Subject: [PATCH 07/49] up baron --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index f2a007fb..67fee6b0 100644 --- a/bower.json +++ b/bower.json @@ -24,7 +24,7 @@ "private": true, "dependencies": { "backbone": "~1.2.3", - "baron": "~0.7.11", + "baron": "~1.0.1", "bourbon": "~4.2.5", "dropbox": "antelle/dropbox-js#0.10.5", "font-awesome": "~4.4.0", From f831ce11d79d7ca445dc95ca094653ba58a6a45c Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 4 Dec 2015 23:22:49 +0300 Subject: [PATCH 08/49] fix #55: workarounds for bugs in custom scrollbar component (baron) in ff/win and ie+edge --- app/styles/areas/_details.scss | 7 +++++++ app/styles/common/_scroll.scss | 23 ++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/styles/areas/_details.scss b/app/styles/areas/_details.scss index 93ae878a..83b15eff 100644 --- a/app/styles/areas/_details.scss +++ b/app/styles/areas/_details.scss @@ -130,6 +130,13 @@ @include flex-wrap(wrap); overflow-x: hidden; padding-top: 3px; + + // workaround for bugs in custom scrollbar component (baron) + width: auto !important; + min-width: 0 !important; + max-width: none !important; + @-moz-document url-prefix() { @include scrollbar-padding-hack(); } + @at-root { _:-ms-lang(x), .details__body>.scroller { @include scrollbar-padding-hack(); } } } &-fields { diff --git a/app/styles/common/_scroll.scss b/app/styles/common/_scroll.scss index b1b1145f..0e74817c 100644 --- a/app/styles/common/_scroll.scss +++ b/app/styles/common/_scroll.scss @@ -1,12 +1,16 @@ +@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; +} + .scroller { overflow-y: scroll; height: 100%; @-moz-document url-prefix() { - // until Firefox bug is fixed in baron, hide native scroll area manually - // https://github.com/Diokuz/baron/issues/91 - // https://github.com/Diokuz/baron/issues/93 - margin-right: -30px; - padding-right: 30px; + @include scrollbar-padding-hack(); } &::-webkit-scrollbar { width: 0; @@ -31,6 +35,15 @@ } } +@media screen and (-moz-windows-theme) { + .scroller { + @-moz-document url-prefix() { + margin-right: 0 !important; + padding-right: 0 !important; + } + } +} + @mixin scrollbar-on-hover { .scroller__bar-wrapper { >.scroller__bar { From f11cf25dbe1f3f9e7b3748afa5e1b8461c8d9d13 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 4 Dec 2015 23:34:09 +0300 Subject: [PATCH 09/49] release notes --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 3ad9f749..6d7c9fea 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,6 +5,7 @@ Release notes `*` disallow opening same files twice `+` #46: option to show colorful icons `+` #45: optional auto-lock on minimize +`-` #55: custom scrollbar issues ##### v0.4.6 (2015-11-25) `-` #32: visual glitches on Windows 10 From db062cec2703d63c2e3cf019239338f704c40c24 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 5 Dec 2015 16:04:09 +0300 Subject: [PATCH 10/49] more stable reload after db operations --- app/scripts/models/app-model.js | 9 +++ app/scripts/models/entry-model.js | 35 +++++---- app/scripts/models/file-model.js | 50 ++++++++---- app/scripts/models/group-model.js | 76 +++++++++---------- app/scripts/models/menu/menu-section-model.js | 6 +- app/scripts/views/menu/menu-item-view.js | 1 + 6 files changed, 108 insertions(+), 69 deletions(-) diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index e73e13c3..78c14463 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -44,9 +44,18 @@ var AppModel = Backbone.Model.extend({ file: file }); this.refresh(); + this.listenTo(file, 'reload', this.reloadFile); return true; }, + reloadFile: function(file) { + this.menu.groupsSection.removeByFile(file, true); + file.get('groups').forEach(function (group) { + this.menu.groupsSection.addItem(group); + }, this); + this.updateTags(); + }, + _addTags: function(group) { var tagsHash = {}; this.tags.forEach(function(tag) { diff --git a/app/scripts/models/entry-model.js b/app/scripts/models/entry-model.js index 81f4ab37..e3b37f8e 100644 --- a/app/scripts/models/entry-model.js +++ b/app/scripts/models/entry-model.js @@ -16,15 +16,18 @@ var EntryModel = Backbone.Model.extend({ }, setEntry: function(entry, group, file) { - this.set({ id: entry.uuid.id }, {silent: true}); this.entry = entry; this.group = group; this.file = file; + if (this.id === entry.uuid.id) { + this._checkUpdatedEntry(); + } this._fillByEntry(); }, _fillByEntry: function() { var entry = this.entry; + this.set({id: entry.uuid.id}, {silent: true}); this.fileName = this.file.db.meta.name; this.title = entry.fields.Title || ''; this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString(''); @@ -48,6 +51,15 @@ var EntryModel = Backbone.Model.extend({ this._buildSearchColor(); }, + _checkUpdatedEntry: function() { + if (this.isJustCreated) { + this.isJustCreated = false; + } + if (this.unsaved && +this.updated !== +this.entry.times.lastModTime) { + this.unsaved = false; + } + }, + _buildSearchText: function() { var text = ''; _.forEach(this.entry.fields, function(value) { @@ -114,7 +126,8 @@ var EntryModel = Backbone.Model.extend({ }, matches: function(filter) { - return (!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) && + return !filter || + (!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) && (!filter.textLower || this.searchText.indexOf(filter.textLower) >= 0) && (!filter.color || filter.color === true && this.searchColor || this.searchColor === filter.color); }, @@ -190,7 +203,7 @@ var EntryModel = Backbone.Model.extend({ deleteHistory: function(historyEntry) { var ix = this.entry.history.indexOf(historyEntry); if (ix >= 0) { - this.entry.history.splice(ix, 1); + this.entry.removeHistory(ix); } this._fillByEntry(); }, @@ -211,9 +224,10 @@ var EntryModel = Backbone.Model.extend({ }, discardUnsaved: function() { - if (this.unsaved) { + if (this.unsaved && this.entry.history.length) { this.unsaved = false; - var historyEntry = this.entry.history.pop(); + var historyEntry = this.entry.history[this.entry.history.length - 1]; + this.entry.removeHistory(this.entry.history.length - 1); this.entry.fields = {}; this.entry.binaries = {}; this.entry.copyFrom(historyEntry); @@ -227,18 +241,13 @@ var EntryModel = Backbone.Model.extend({ this.isJustCreated = false; } this.file.db.remove(this.entry); - this.group.removeEntry(this); - var trashGroup = this.file.getTrashGroup(); - if (trashGroup) { - trashGroup.addEntry(this); - this.group = trashGroup; - } + this.file.reload(); }, deleteFromTrash: function() { this.file.setModified(); this.file.db.move(this.entry, null); - this.group.removeEntry(this); + this.file.reload(); }, removeWithoutHistory: function() { @@ -246,7 +255,7 @@ var EntryModel = Backbone.Model.extend({ if (ix >= 0) { this.group.group.entries.splice(ix, 1); } - this.group.removeEntry(this); + this.file.reload(); } }); diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 5a4c216c..b0dbdec6 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -35,8 +35,12 @@ var FileModel = Backbone.Model.extend({ db: null, data: null, + entryMap: null, + groupMap: null, initialize: function() { + this.entryMap = {}; + this.groupMap = {}; }, open: function(password, fileData, keyFileData) { @@ -152,13 +156,38 @@ var FileModel = Backbone.Model.extend({ historyMaxSize: this.db.meta.historyMaxSize, keyEncryptionRounds: this.db.header.keyEncryptionRounds }, { silent: true }); - this.db.groups.forEach(function(group, index) { - var groupModel = GroupModel.fromGroup(group, this); - if (index === 0 && topGroupTitle) { + this.db.groups.forEach(function(group) { + var groupModel = this.getGroup(group.uuid.id); + if (groupModel) { + groupModel.setGroup(group, this); + } else { + groupModel = GroupModel.fromGroup(group, this); + } + if (topGroupTitle) { groupModel.set({title: topGroupTitle}); } groups.add(groupModel); }, this); + this.buildObjectMap(); + }, + + buildObjectMap: function() { + var entryMap = {}; + var groupMap = {}; + this.forEachGroup(function(group) { + groupMap[group.id] = group; + group.forEachOwnEntry(null, function(entry) { + entryMap[entry.id] = entry; + }); + }, true); + this.entryMap = entryMap; + this.groupMap = groupMap; + }, + + reload: function() { + this.buildObjectMap(); + this.readModel(this.get('name')); + this.trigger('reload', this); }, close: function() { @@ -177,17 +206,12 @@ var FileModel = Backbone.Model.extend({ }); }, + getEntry: function(id) { + return this.entryMap[id]; + }, + getGroup: function(id) { - var found = null; - if (id) { - this.forEachGroup(function (group) { - if (group.get('id') === id) { - found = group; - return false; - } - }, true); - } - return found; + return this.groupMap[id]; }, forEachEntry: function(filter, callback) { diff --git a/app/scripts/models/group-model.js b/app/scripts/models/group-model.js index 936fe1c1..85554965 100644 --- a/app/scripts/models/group-model.js +++ b/app/scripts/models/group-model.js @@ -22,28 +22,43 @@ var GroupModel = MenuItemModel.extend({ initialize: function() { if (!GroupCollection) { GroupCollection = require('../collections/group-collection'); } if (!EntryCollection) { EntryCollection = require('../collections/entry-collection'); } - this.set('entries', new EntryCollection()); }, - setFromGroup: function(group, file) { + setGroup: function(group, file, parentGroup) { var isRecycleBin = file.db.meta.recycleBinUuid && file.db.meta.recycleBinUuid.id === group.uuid.id; this.set({ id: group.uuid.id, - expanded: true, + expanded: group.expanded, visible: !isRecycleBin, items: new GroupCollection(), - filterValue: group.uuid.id + entries: new EntryCollection(), + filterValue: group.uuid.id, + top: !parentGroup, + drag: !!parentGroup }, { silent: true }); this.group = group; this.file = file; + this.parentGroup = parentGroup; this._fillByGroup(true); var items = this.get('items'), entries = this.get('entries'); group.groups.forEach(function(subGroup) { - items.add(GroupModel.fromGroup(subGroup, file, this)); + var existing = file.getGroup(subGroup.uuid); + if (existing) { + existing.setGroup(subGroup, file, this); + items.add(existing); + } else { + items.add(GroupModel.fromGroup(subGroup, file, this)); + } }, this); group.entries.forEach(function(entry) { - entries.add(EntryModel.fromEntry(entry, this, file)); + var existing = file.getEntry(entry.uuid); + if (existing) { + existing.setEntry(entry, this, file); + entries.add(existing); + } else { + entries.add(EntryModel.fromEntry(entry, this, file)); + } }, this); }, @@ -102,22 +117,12 @@ var GroupModel = MenuItemModel.extend({ return this.group.groups; }, - removeEntry: function(entry) { - this.get('entries').remove(entry); - }, - addEntry: function(entry) { this.get('entries').add(entry); }, - removeGroup: function(group) { - this.get('items').remove(group); - this.trigger('remove', group); - }, - addGroup: function(group) { this.get('items').add(group); - this.trigger('insert', group); }, setName: function(name) { @@ -139,21 +144,21 @@ var GroupModel = MenuItemModel.extend({ this._fillByGroup(); }, + setExpanded: function(expanded) { + this._groupModified(); + this.group.expanded = expanded; + this.set('expanded', expanded); + }, + moveToTrash: function() { this.file.setModified(); this.file.db.remove(this.group); - this.parentGroup.removeGroup(this); - var trashGroup = this.file.getTrashGroup(); - if (trashGroup) { - trashGroup.addGroup(this); - this.parentGroup = trashGroup; - } - this.trigger('delete'); + this.file.reload(); }, deleteFromTrash: function() { this.file.db.move(this.group, null); - this.parentGroup.removeGroup(this); + this.file.reload(); }, removeWithoutHistory: function() { @@ -161,8 +166,7 @@ var GroupModel = MenuItemModel.extend({ if (ix >= 0) { this.parentGroup.group.groups.splice(ix, 1); } - this.parentGroup.removeGroup(this); - this.trigger('delete'); + this.file.reload(); }, moveHere: function(object) { @@ -175,42 +179,32 @@ var GroupModel = MenuItemModel.extend({ return; } this.file.db.move(object.group, this.group); - object.parentGroup.removeGroup(object); - object.trigger('delete'); - this.addGroup(object); + this.file.reload(); } else if (object instanceof EntryModel) { if (this.group.entries.indexOf(object.entry) >= 0) { return; } this.file.db.move(object.entry, this.group); - object.group.removeEntry(object); - this.addEntry(object); - object.group = this; + this.file.reload(); } } }); GroupModel.fromGroup = function(group, file, parentGroup) { var model = new GroupModel(); - model.setFromGroup(group, file); - if (parentGroup) { - model.parentGroup = parentGroup; - } else { - model.set({ top: true, drag: false }, { silent: true }); - } + model.setGroup(group, file, parentGroup); return model; }; GroupModel.newGroup = function(group, file) { var model = new GroupModel(); var grp = file.db.createGroup(group.group); - model.setFromGroup(grp, file); + model.setGroup(grp, file, group); model.group.times.update(); - model.parentGroup = group; - model.unsaved = true; model.isJustCreated = true; group.addGroup(model); file.setModified(); + file.reload(); return model; }; diff --git a/app/scripts/models/menu/menu-section-model.js b/app/scripts/models/menu/menu-section-model.js index 2006e870..7c599587 100644 --- a/app/scripts/models/menu/menu-section-model.js +++ b/app/scripts/models/menu/menu-section-model.js @@ -27,7 +27,7 @@ var MenuItemModel = Backbone.Model.extend({ this.trigger('change-items'); }, - removeByFile: function(file) { + removeByFile: function(file, skipEvent) { var items = this.get('items'); var toRemove; items.each(function(item) { @@ -38,7 +38,9 @@ var MenuItemModel = Backbone.Model.extend({ if (toRemove) { items.remove(toRemove); } - this.trigger('change-items'); + if (!skipEvent) { + this.trigger('change-items'); + } }, setItems: function(items) { diff --git a/app/scripts/views/menu/menu-item-view.js b/app/scripts/views/menu/menu-item-view.js index b20a0a90..e24be358 100644 --- a/app/scripts/views/menu/menu-item-view.js +++ b/app/scripts/views/menu/menu-item-view.js @@ -96,6 +96,7 @@ var MenuItemView = Backbone.View.extend({ changeExpanded: function(model, expanded) { this.$el.toggleClass('menu__item--collapsed', !expanded); + this.model.setExpanded(expanded); }, changeCls: function(model, cls) { From 6aa32b1dc01b3f6c9d14f852088052eabc801e15 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 5 Dec 2015 16:57:43 +0300 Subject: [PATCH 11/49] settings store --- app/scripts/comp/settings-store.js | 41 +++++++++++++++++++ app/scripts/models/app-settings-model.js | 35 ++++------------ app/scripts/models/update-model.js | 9 ++-- app/scripts/util/string-util.js | 11 +++++ .../views/settings/settings-general-view.js | 6 +-- app/styles/themes/_all-themes.scss | 2 +- .../{_default.scss => _dark-brown.scss} | 2 +- 7 files changed, 69 insertions(+), 37 deletions(-) create mode 100644 app/scripts/comp/settings-store.js create mode 100644 app/scripts/util/string-util.js rename app/styles/themes/{_default.scss => _dark-brown.scss} (95%) diff --git a/app/scripts/comp/settings-store.js b/app/scripts/comp/settings-store.js new file mode 100644 index 00000000..2c53ea24 --- /dev/null +++ b/app/scripts/comp/settings-store.js @@ -0,0 +1,41 @@ +'use strict'; + +var Launcher = require('./launcher'), + StringUtil = require('../util/string-util'); + +var SettingsStore = { + fileName: function(key) { + return key + '.json'; + }, + + load: function(key) { + try { + if (Launcher) { + var settingsFile = Launcher.getUserDataPath(this.fileName(key)); + if (Launcher.fileExists(settingsFile)) { + return JSON.parse(Launcher.readFile(settingsFile, 'utf8')); + } + } else { + var data = localStorage[StringUtil.camelCase(key)]; + return JSON.parse(data); + } + } catch (e) { + console.error('Error loading ' + key, e); + } + return null; + }, + + save: function(key, data) { + try { + if (Launcher) { + Launcher.writeFile(Launcher.getUserDataPath(this.fileName(key)), JSON.stringify(data)); + } else if (typeof localStorage !== 'undefined') { + localStorage[StringUtil.camelCase(key)] = JSON.stringify(data); + } + } catch (e) { + console.error('Error saving ' + key, e); + } + } +}; + +module.exports = SettingsStore; diff --git a/app/scripts/models/app-settings-model.js b/app/scripts/models/app-settings-model.js index 96671a12..6e782233 100644 --- a/app/scripts/models/app-settings-model.js +++ b/app/scripts/models/app-settings-model.js @@ -1,13 +1,11 @@ 'use strict'; var Backbone = require('backbone'), - Launcher = require('../comp/launcher'); - -var FileName = 'app-settings.json'; + SettingsStore = require('../comp/settings-store'); var AppSettingsModel = Backbone.Model.extend({ defaults: { - theme: 'd', + theme: 'fb', expandGroups: true, listViewWidth: null, menuViewWidth: null, @@ -27,34 +25,15 @@ var AppSettingsModel = Backbone.Model.extend({ }, load: function() { - try { - var data; - if (Launcher) { - 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); - } - if (data) { - this.set(data, {silent: true}); - } - } catch (e) { - console.error('Error loading settings', e); + var data = SettingsStore.load('app-settings'); + if (data) { + if (data.theme === 'd') { data.theme = 'db'; } // TODO: remove in v0.6 + this.set(data, {silent: true}); } }, save: function() { - try { - if (Launcher) { - Launcher.writeFile(Launcher.getUserDataPath(FileName), JSON.stringify(this.attributes)); - } else if (typeof localStorage !== 'undefined') { - localStorage.appSettings = JSON.stringify(this.attributes); - } - } catch (e) { - console.error('Error saving settings', e); - } + SettingsStore.save('app-settings', this.attributes); } }); diff --git a/app/scripts/models/update-model.js b/app/scripts/models/update-model.js index 00567524..fc4227e7 100644 --- a/app/scripts/models/update-model.js +++ b/app/scripts/models/update-model.js @@ -1,6 +1,7 @@ 'use strict'; -var Backbone = require('backbone'); +var Backbone = require('backbone'), + SettingsStore = require('../comp/settings-store'); var UpdateModel = Backbone.Model.extend({ defaults: { @@ -19,9 +20,9 @@ var UpdateModel = Backbone.Model.extend({ }, load: function() { - if (localStorage.updateInfo) { + var data = SettingsStore.load('update-info'); + if (data) { try { - var data = JSON.parse(localStorage.updateInfo); _.each(data, function(val, key) { if (/Date$/.test(key)) { data[key] = val ? new Date(val) : null; @@ -39,7 +40,7 @@ var UpdateModel = Backbone.Model.extend({ delete attr[key]; } }); - localStorage.updateInfo = JSON.stringify(attr); + SettingsStore.save('update-info', attr); } }); diff --git a/app/scripts/util/string-util.js b/app/scripts/util/string-util.js new file mode 100644 index 00000000..0f7636e2 --- /dev/null +++ b/app/scripts/util/string-util.js @@ -0,0 +1,11 @@ +'use strict'; + +var StringUtil = { + camelCaseRegex: /\-./g, + + camelCase: function(str) { + return str.replace(this.camelCaseRegex, function(match) { return match[1].toUpperCase(); }); + } +}; + +module.exports = StringUtil; diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index a3fad74d..a6e61c35 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -32,9 +32,9 @@ var SettingsGeneralView = Backbone.View.extend({ }, allThemes: { - d: 'default', - fb: 'flat blue', - wh: 'white' + fb: 'Flat blue', + db: 'Dark brown', + wh: 'White' }, initialize: function() { diff --git a/app/styles/themes/_all-themes.scss b/app/styles/themes/_all-themes.scss index 5bd55d16..24d1b4a7 100644 --- a/app/styles/themes/_all-themes.scss +++ b/app/styles/themes/_all-themes.scss @@ -1,5 +1,5 @@ $themes: (); -@import "default"; +@import "dark-brown"; @import "flat-blue"; @import "white"; diff --git a/app/styles/themes/_default.scss b/app/styles/themes/_dark-brown.scss similarity index 95% rename from app/styles/themes/_default.scss rename to app/styles/themes/_dark-brown.scss index d2d0d8ea..909511f2 100644 --- a/app/styles/themes/_default.scss +++ b/app/styles/themes/_dark-brown.scss @@ -1,5 +1,5 @@ $themes: map-merge($themes, ( - d: ( + db: ( background-color: #342F2E, medium-color: #FED9D8, text-color: #FFEAE9, From 83b95eaa506ee38875b31ce7d3618631dc2f92d3 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 5 Dec 2015 17:02:24 +0300 Subject: [PATCH 12/49] fix link --- app/templates/settings/settings-help.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/settings/settings-help.html b/app/templates/settings/settings-help.html index 8cc5db6a..834022c9 100644 --- a/app/templates/settings/settings-help.html +++ b/app/templates/settings/settings-help.html @@ -29,5 +29,5 @@

Updates

-

App twitter: kee_web

+

App twitter: kee_web

From 2a7ebcf20d23ebde68b4650b9608533a1565717a Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 5 Dec 2015 19:11:08 +0300 Subject: [PATCH 13/49] remove unused version --- Gruntfile.js | 3 +-- app/manifest.appcache | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index ec088016..60847ae5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -152,8 +152,7 @@ module.exports = function(grunt) { manifest: { options: { replacements: [ - { pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version }, - { pattern: 'vElectron', replacement: electronVersion } + { pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version } ] }, files: { 'dist/manifest.appcache': 'app/manifest.appcache' } diff --git a/app/manifest.appcache b/app/manifest.appcache index 2d741eb0..0f8b8573 100644 --- a/app/manifest.appcache +++ b/app/manifest.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# YYYY-MM-DD:v0.0.0 evElectron +# YYYY-MM-DD:v0.0.0 CACHE: index.html From e812f08c03602fa5d0db1f0a0e4ba8f1c5d76f1e Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 5 Dec 2015 22:09:31 +0300 Subject: [PATCH 14/49] release notes --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index 6d7c9fea..28db7bf8 100644 --- a/release-notes.md +++ b/release-notes.md @@ -3,6 +3,7 @@ Release notes ##### v0.5 (not released yet) 2-way sync `*` disallow opening same files twice +`*` default theme is now blue `+` #46: option to show colorful icons `+` #45: optional auto-lock on minimize `-` #55: custom scrollbar issues From 2998e3f6143189c369797f3ba3025bfe718b9107 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sun, 6 Dec 2015 23:32:41 +0300 Subject: [PATCH 15/49] file open refactoring --- Gruntfile.js | 5 + app/index.html | 2 +- app/scripts/app.js | 11 +- .../collections/file-info-collection.js | 35 ++ app/scripts/comp/last-open-files.js | 55 -- app/scripts/comp/launcher.js | 3 + app/scripts/comp/settings-store.js | 2 +- app/scripts/models/app-model.js | 88 +++- app/scripts/models/file-info-model.js | 28 + app/scripts/models/file-model.js | 47 +- app/scripts/storage/index.js | 4 +- app/scripts/storage/storage-dropbox.js | 2 +- app/scripts/storage/storage-file-cache.js | 88 ++++ app/scripts/views/app-view.js | 17 +- app/scripts/views/open-view.js | 489 ++++++++---------- app/templates/open.html | 2 +- 16 files changed, 499 insertions(+), 379 deletions(-) create mode 100644 app/scripts/collections/file-info-collection.js delete mode 100644 app/scripts/comp/last-open-files.js create mode 100644 app/scripts/models/file-info-model.js create mode 100644 app/scripts/storage/storage-file-cache.js diff --git a/Gruntfile.js b/Gruntfile.js index 60847ae5..68b31d84 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -157,6 +157,10 @@ module.exports = function(grunt) { }, files: { 'dist/manifest.appcache': 'app/manifest.appcache' } }, + 'manifest_html': { + options: { replacements: [{ pattern: ' - + KeeWeb diff --git a/app/scripts/app.js b/app/scripts/app.js index cc68841d..87919020 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -7,7 +7,6 @@ var AppModel = require('./models/app-model'), Alerts = require('./comp/alerts'), DropboxLink = require('./comp/dropbox-link'), Updater = require('./comp/updater'), - LastOpenFiles = require('./comp/last-open-files'), ThemeChanger = require('./util/theme-changer'); $(function() { @@ -41,15 +40,7 @@ $(function() { } function showApp() { - var appView = new AppView({ model: appModel }).render(); - - var lastOpenFiles = LastOpenFiles.all(); - var lastOpenFile = lastOpenFiles[0]; - if (lastOpenFile && lastOpenFile.storage === 'file' && lastOpenFile.path) { - appView.showOpenFile(lastOpenFile.path); - } else { - appView.showOpenFile(); - } + new AppView({ model: appModel }).render(); Updater.init(); } }); diff --git a/app/scripts/collections/file-info-collection.js b/app/scripts/collections/file-info-collection.js new file mode 100644 index 00000000..d997545c --- /dev/null +++ b/app/scripts/collections/file-info-collection.js @@ -0,0 +1,35 @@ +'use strict'; + +var Backbone = require('backbone'), + FileInfoModel = require('../models/file-info-model'), + SettingsStore = require('../comp/settings-store'); + +var FileInfoCollection = Backbone.Collection.extend({ + model: FileInfoModel, + + initialize: function () { + }, + + load: function () { + var data = SettingsStore.load('file-info'); + if (data) { + this.reset(data, {silent: true}); + } + }, + + save: function () { + SettingsStore.save('file-info', this.toJSON()); + }, + + getLast: function () { + this.max(function(file) { return file.get('openDate'); }); + } +}); + +FileInfoCollection.load = function() { + var coll = new FileInfoCollection(); + coll.load(); + return coll; +}; + +module.exports = FileInfoCollection; diff --git a/app/scripts/comp/last-open-files.js b/app/scripts/comp/last-open-files.js deleted file mode 100644 index 304e52ec..00000000 --- a/app/scripts/comp/last-open-files.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -var Storage = require('../storage'); - -var MaxItems = 5; - -var LastOpenFiles = { - all: function() { - try { - return JSON.parse(localStorage.lastOpenFiles).map(function(f) { - f.dt = Date.parse(f.dt); - return f; - }); - } catch (e) { - return []; - } - }, - - byName: function(name) { - return this.all().filter(function(f) { return f.name === name; })[0]; - }, - - save: function(files) { - try { - localStorage.lastOpenFiles = JSON.stringify(files); - } catch (e) { - console.error('Error saving last open files', e); - } - }, - - add: function(name, storage, path, availOffline) { - console.log('Add last open file', name, storage, path); - var files = this.all(); - files = files.filter(function(f) { return f.name !== name; }); - files.unshift({ name: name, storage: storage, path: path, availOffline: availOffline, dt: new Date() }); - while (files.length > MaxItems) { - files.pop(); - } - this.save(files); - }, - - remove: function(name) { - console.log('Remove last open file', name); - var files = this.all(); - files.forEach(function(file) { - if (file.name === name && file.availOffline) { - Storage.cache.remove(file.name); - } - }, this); - files = files.filter(function(file) { return file.name !== name; }); - this.save(files); - } -}; - -module.exports = LastOpenFiles; diff --git a/app/scripts/comp/launcher.js b/app/scripts/comp/launcher.js index bb3ce0c8..093ee972 100644 --- a/app/scripts/comp/launcher.js +++ b/app/scripts/comp/launcher.js @@ -49,6 +49,9 @@ if (window.process && window.process.versions && window.process.versions.electro fileExists: function(path) { return this.req('fs').existsSync(path); }, + deleteFile: function(path) { + this.req('fs').unlinkSync(path); + }, preventExit: function(e) { e.returnValue = false; return false; diff --git a/app/scripts/comp/settings-store.js b/app/scripts/comp/settings-store.js index 2c53ea24..37313d0f 100644 --- a/app/scripts/comp/settings-store.js +++ b/app/scripts/comp/settings-store.js @@ -17,7 +17,7 @@ var SettingsStore = { } } else { var data = localStorage[StringUtil.camelCase(key)]; - return JSON.parse(data); + return data ? JSON.parse(data) : undefined; } } catch (e) { console.error('Error loading ' + key, e); diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 78c14463..c34cedaf 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -6,7 +6,11 @@ var Backbone = require('backbone'), EntryModel = require('./entry-model'), GroupModel = require('./group-model'), FileCollection = require('../collections/file-collection'), - EntryCollection = require('../collections/entry-collection'); + EntryCollection = require('../collections/entry-collection'), + FileInfoCollection = require('../collections/file-info-collection'), + FileModel = require('./file-model'), + FileInfoModel = require('./file-info-model'), + Storage = require('../storage'); var AppModel = Backbone.Model.extend({ defaults: {}, @@ -14,6 +18,7 @@ var AppModel = Backbone.Model.extend({ initialize: function() { this.tags = []; this.files = new FileCollection(); + this.fileInfos = FileInfoCollection.load(); this.menu = new MenuModel(); this.filter = {}; this.sort = 'title'; @@ -28,7 +33,7 @@ var AppModel = Backbone.Model.extend({ }, addFile: function(file) { - if (this.files.getById(file.get('id'))) { + if (this.files.getById(file.id)) { return false; } this.files.add(file); @@ -212,6 +217,85 @@ var AppModel = Backbone.Model.extend({ createNewGroup: function() { var sel = this.getFirstSelectedGroup(); return GroupModel.newGroup(sel.group, sel.file); + }, + + createDemoFile: function() { + var that = this; + if (!this.files.getByName('Demo')) { + var demoFile = new FileModel(); + demoFile.openDemo(function() { + that.addFile(demoFile); + }); + return true; + } else { + return false; + } + }, + + createNewFile: function() { + var name; + for (var i = 0; ; i++) { + name = 'New' + (i || ''); + if (!this.files.getByName(name)) { + break; + } + } + var newFile = new FileModel(); + newFile.create(name); + this.addFile(newFile); + }, + + openFile: function(params, callback) { + var that = this; + var file = new FileModel({ + name: params.name, + availOffline: params.availOffline, + storage: params.storage, + path: params.path, + keyFileName: params.keyFileName + }); + file.open(params.password, params.fileData, params.keyFileData, function(err) { + if (err || that.files.get(file.id)) { + return callback(err); + } + if (!params.offline) { + if (params.availOffline) { + Storage.cache.save(file.id, params.fileData, function(err) { + if (err) { + file.set('availOffline', false); + } + that.addToLastOpenFiles(file); + }); + } else { + Storage.cache.remove(file.id); + } + } + that.addFile(file); + }); + }, + + addToLastOpenFiles: function(file) { + var fileInfo = new FileInfoModel({ + id: file.id, + name: file.get('name'), + storage: file.get('storage'), + path: file.get('path'), + availOffline: file.get('availOffline'), + modified: file.get('modified'), + editState: null, + pullRev: null, + pullDate: null, + openDate: new Date() + }); + this.fileInfos.remove(file.id); + this.fileInfos.push(fileInfo); + this.fileInfos.save(); + }, + + removeFileInfo: function(id) { + Storage.cache.remove(id); + this.fileInfos.remove(id); + this.fileInfos.save(); } }); diff --git a/app/scripts/models/file-info-model.js b/app/scripts/models/file-info-model.js new file mode 100644 index 00000000..b165d3e4 --- /dev/null +++ b/app/scripts/models/file-info-model.js @@ -0,0 +1,28 @@ +'use strict'; + +var Backbone = require('backbone'); + +var FileInfoModel = Backbone.Model.extend({ + defaults: { + id: '', + name: '', + storage: null, + path: null, + availOffline: false, + modified: false, + editState: null, + pullRev: null, + pullDate: null, + openDate: null + }, + + initialize: function(data, options) { + _.each(data, function(val, key) { + if (/Date$/.test(key)) { + this.set(key, val ? new Date(val) : null, options); + } + }, this); + } +}); + +module.exports = FileInfoModel; diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index b0dbdec6..251d0987 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -4,7 +4,6 @@ var Backbone = require('backbone'), GroupCollection = require('../collections/group-collection'), GroupModel = require('./group-model'), Storage = require('../storage'), - LastOpenFiles = require('../comp/last-open-files'), IconUrl = require('../util/icon-url'), kdbxweb = require('kdbxweb'), demoFileData = require('base64!../../resources/Demo.kdbx'); @@ -19,8 +18,6 @@ var FileModel = Backbone.Model.extend({ storage: null, modified: false, open: false, - opening: false, - error: false, created: false, demo: false, groups: null, @@ -43,7 +40,7 @@ var FileModel = Backbone.Model.extend({ this.groupMap = {}; }, - open: function(password, fileData, keyFileData) { + open: function(password, fileData, keyFileData, callback) { var len = password.value.length, byteLength = 0, value = new Uint8Array(len * 4), @@ -63,8 +60,8 @@ var FileModel = Backbone.Model.extend({ var start = performance.now(); kdbxweb.Kdbx.load(fileData, credentials, (function(db, err) { if (err) { - this.set({error: true, opening: false}); console.error('Error opening file', err.code, err.message, err); + callback(err); } else { this.db = db; this.readModel(this.get('name')); @@ -74,51 +71,24 @@ var FileModel = Backbone.Model.extend({ } console.log('Opened file ' + this.get('name') + ': ' + Math.round(performance.now() - start) + 'ms, ' + db.header.keyEncryptionRounds + ' rounds, ' + Math.round(fileData.byteLength / 1024) + ' kB'); - this.postOpen(fileData); + callback(); } }).bind(this)); } catch (e) { console.error('Error opening file', e, e.code, e.message, e); - this.set({ error: true, opening: false }); + callback(e); } }, - postOpen: function(fileData) { - var that = this; - this.data = fileData; - if (!this.get('offline')) { - if (this.get('availOffline')) { - Storage.cache.save(this.get('name'), fileData, function (err) { - if (err) { - that.set('availOffline', false); - if (!that.get('storage')) { - return; - } - } - that.addToLastOpenFiles(!err); - }); - } else { - if (this.get('storage')) { - this.addToLastOpenFiles(false); - } - Storage.cache.remove(this.get('name')); - } - } - }, - - addToLastOpenFiles: function(hasOfflineCache) { - LastOpenFiles.add(this.get('name'), this.get('storage'), this.get('path'), hasOfflineCache); - }, - create: function(name) { var password = kdbxweb.ProtectedValue.fromString(''); var credentials = new kdbxweb.Credentials(password); this.db = kdbxweb.Kdbx.create(credentials, name); this.readModel(); - this.set({ open: true, created: true, opening: false, error: false, name: name, offline: false }); + this.set({ open: true, created: true, name: name, offline: false }); }, - createDemo: function() { + openDemo: function(callback) { var password = kdbxweb.ProtectedValue.fromString('demo'); var credentials = new kdbxweb.Credentials(password); var demoFile = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(demoFileData)); @@ -126,14 +96,13 @@ var FileModel = Backbone.Model.extend({ this.db = db; this.readModel(); this.setOpenFile({passwordLength: 4, demo: true, name: 'Demo' }); + callback(); }).bind(this)); }, setOpenFile: function(props) { _.extend(props, { open: true, - opening: false, - error: false, oldKeyFileName: this.get('keyFileName'), oldPasswordLength: props.passwordLength, passwordChanged: false, @@ -196,8 +165,6 @@ var FileModel = Backbone.Model.extend({ passwordLength: 0, modified: false, open: false, - opening: false, - error: false, created: false, groups: null, passwordChanged: false, diff --git a/app/scripts/storage/index.js b/app/scripts/storage/index.js index c698a550..0b43c9f5 100644 --- a/app/scripts/storage/index.js +++ b/app/scripts/storage/index.js @@ -1,9 +1,11 @@ 'use strict'; +var Launcher = require('../comp/launcher'); + var Storage = { file: require('./storage-file'), dropbox: require('./storage-dropbox'), - cache: require('./storage-cache') + cache: Launcher ? require('./storage-file-cache') : require('./storage-cache') }; module.exports = Storage; diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js index 8c88c8c6..77e7bad0 100644 --- a/app/scripts/storage/storage-dropbox.js +++ b/app/scripts/storage/storage-dropbox.js @@ -7,7 +7,7 @@ var StorageDropbox = { enabled: true, load: function(path, callback) { - DropboxLink.openFile(path, callback); + DropboxLink.openUploadedFile(path, callback); }, save: function(path, data, callback) { diff --git a/app/scripts/storage/storage-file-cache.js b/app/scripts/storage/storage-file-cache.js new file mode 100644 index 00000000..9a3cb1e9 --- /dev/null +++ b/app/scripts/storage/storage-file-cache.js @@ -0,0 +1,88 @@ +'use strict'; + +var Launcher = require('../comp/launcher'); + +var StorageFileCache = { + name: 'cache', + enabled: !!Launcher, + + path: null, + + getPath: function(id) { + // get safe file name by base64 as described in RFC3548: http://tools.ietf.org/html/rfc3548#page-6 + id = id.replace(/\//g, '_').replace(/\+/g, '-'); + return Launcher.req('path').join(this.path, id); + }, + + init: function(callback) { + if (this.path) { + return callback(); + } + if (Launcher) { + try { + var path = Launcher.getUserDataPath('OfflineFiles'); + var fs = Launcher.req('fs'); + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + } + this.path = path; + } catch (e) { + console.error('Error opening local offline storage', e); + callback(e); + } + } + }, + + save: function(id, data, callback) { + this.init((function(err) { + if (err) { + return callback(err); + } + try { + if (Launcher) { + Launcher.writeFile(this.getPath(id), data); + return callback(); + } + } catch (e) { + console.error('Error saving to cache', id, e); + callback(e); + } + }).bind(this)); + }, + + load: function(id, callback) { + this.init((function(err) { + if (err) { + return callback(null, err); + } + try { + if (Launcher) { + var data = Launcher.readFile(this.getPath(id)); + return callback(data.buffer); + } + } catch (e) { + console.error('Error loading from cache', id, e); + callback(null, e); + } + }).bind(this)); + }, + + remove: function(id, callback) { + this.init((function(err) { + if (err) { + return callback(err); + } + try { + if (Launcher) { + Launcher.deleteFile(this.getPath(id)); + return callback(); + } + } catch(e) { + console.error('Error removing from cache', id, e); + callback(e); + } + }).bind(this)); + } +}; + +module.exports = StorageFileCache; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index 0fdf70dc..91397cfe 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -86,10 +86,11 @@ var AppView = Backbone.View.extend({ this.views.listDrag.setElement(this.$el.find('.app__list-drag')).render(); this.views.details.setElement(this.$el.find('.app__details')).render(); this.views.grp.setElement(this.$el.find('.app__grp')).render().hide(); + this.showLastOpenFile(); return this; }, - showOpenFile: function(filePath) { + showOpenFile: function() { this.views.menu.hide(); this.views.menuDrag.hide(); this.views.listWrap.hide(); @@ -102,15 +103,21 @@ var AppView = Backbone.View.extend({ this.hideOpenFile(); this.views.open = new OpenView({ model: this.model }); this.views.open.setElement(this.$el.find('.app__body')).render(); - this.views.open.on('cancel', this.showEntries, this); - if (Launcher && filePath) { - this.views.open.showOpenLocalFile(filePath); + this.views.open.on('close', this.showEntries, this); + }, + + showLastOpenFile: function() { + this.showOpenFile(); + var lastOpenFile = this.model.fileInfos.getLast(); + if (lastOpenFile) { + this.views.open.showOpenFileInfo(lastOpenFile); } }, launcherOpenFile: function(path) { if (path && /\.kdbx$/i.test(path)) { - this.showOpenFile(path); + this.showOpenFile(); + this.views.open.showOpenLocalFile(path); } }, diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index c17f3d1b..5e848870 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -4,10 +4,6 @@ var Backbone = require('backbone'), Keys = require('../const/keys'), Alerts = require('../comp/alerts'), SecureInput = require('../comp/secure-input'), - FileModel = require('../models/file-model'), - Launcher = require('../comp/launcher'), - LastOpenFiles = require('../comp/last-open-files'), - Storage = require('../storage'), DropboxLink = require('../comp/dropbox-link'); var OpenView = Backbone.View.extend({ @@ -31,25 +27,25 @@ var OpenView = Backbone.View.extend({ 'drop': 'drop' }, - fileData: null, - keyFileData: null, + params: null, passwordInput: null, - dropboxLoading: null, + busy: false, initialize: function () { - this.setFileModel(new FileModel()); - this.fileData = null; - this.keyFileData = null; + this.params = { + id: null, + name: '', + storage: null, + path: null, + offline: false, + availOffline: false, + keyFileName: null, + keyFileData: null, + fileData: null + }; this.passwordInput = new SecureInput(); }, - setFileModel: function(file) { - this.file = file; - this.listenTo(this.file, 'change:open', this.fileOpenChanged); - this.listenTo(this.file, 'change:opening', this.fileOpeningChanged); - this.listenTo(this.file, 'change:error', this.fileErrorChanged); - }, - render: function () { if (this.dragTimeout) { clearTimeout(this.dragTimeout); @@ -61,16 +57,24 @@ var OpenView = Backbone.View.extend({ }, getLastOpenFiles: function() { - return LastOpenFiles.all().map(function(f) { + return this.model.fileInfos.map(function(f) { + var icon; switch (f.storage) { case 'dropbox': - f.icon = 'dropbox'; + icon = 'dropbox'; + break; + case 'file': + icon = 'hdd-o'; break; default: - f.icon = 'file-text'; + icon = 'file-text'; break; } - return f; + return { + id: f.get('id'), + name: f.get('name'), + icon: icon + }; }); }, @@ -79,32 +83,6 @@ var OpenView = Backbone.View.extend({ Backbone.View.prototype.remove.apply(this, arguments); }, - fileOpenChanged: function() { - if (!this.model.addFile(this.file)) { - this.trigger('cancel'); - } - }, - - fileOpeningChanged: function() { - var opening = this.file.get('opening'); - this.$el.toggleClass('open--opening', opening); - if (opening) { - this.inputEl.attr('disabled', 'disabled'); - this.$el.find('#open__settings-check-offline').attr('disabled', 'disabled'); - } else { - this.inputEl.removeAttr('disabled'); - this.$el.find('#open__settings-check-offline').removeAttr('disabled'); - } - }, - - fileErrorChanged: function() { - if (this.file.get('error')) { - this.inputEl.addClass('input--error').focus(); - this.inputEl[0].selectionStart = 0; - this.inputEl[0].selectionEnd = this.inputEl.val().length; - } - }, - fileSelected: function(e) { var file = e.target.files[0]; if (file) { @@ -115,15 +93,15 @@ var OpenView = Backbone.View.extend({ processFile: function(file, complete) { var reader = new FileReader(); reader.onload = (function(e) { - this[this.reading] = e.target.result; + this.params[this.reading] = e.target.result; if (this.reading === 'fileData') { - this.file.set({ name: file.name.replace(/\.\w+$/i, ''), offline: false }); + _.extend(this.params, { name: file.name.replace(/\.\w+$/i, ''), offline: false }); if (file.path) { - this.file.set({ path: file.path, storage: file.storage || 'file' }); + _.extend(this.params, { path: file.path, storage: file.storage || 'file' }); } this.displayOpenFile(); } else { - this.file.set('keyFileName', file.name); + _.extend(this.params, { keyFileName: file.name }); this.displayOpenKeyFile(); } if (complete) { @@ -132,7 +110,6 @@ var OpenView = Backbone.View.extend({ }).bind(this); reader.onerror = (function() { Alerts.error({ header: 'Failed to read file' }); - this.showReadyToOpen(); if (complete) { complete(false); } @@ -144,16 +121,16 @@ var OpenView = Backbone.View.extend({ this.$el.addClass('open--file'); this.$el.find('.open__settings-key-file').removeClass('hide'); this.$el.find('#open__settings-check-offline')[0].removeAttribute('disabled'); - var canSwitchOffline = this.file.get('storage') !== 'file' && !this.file.get('offline'); + var canSwitchOffline = this.params.storage !== 'file' && !this.params.offline; this.$el.find('.open__settings-offline').toggleClass('hide', !canSwitchOffline); - this.$el.find('.open__settings-offline-warning').toggleClass('hide', !this.file.get('offline')); + this.$el.find('.open__settings-offline-warning').toggleClass('hide', !this.params.offline); this.inputEl[0].removeAttribute('readonly'); - this.inputEl[0].setAttribute('placeholder', 'Password for ' + this.file.get('name')); + this.inputEl[0].setAttribute('placeholder', 'Password for ' + this.params.name); this.inputEl.focus(); }, displayOpenKeyFile: function() { - this.$el.find('.open__settings-key-file-name').text(this.file.get('keyFileName')); + this.$el.find('.open__settings-key-file-name').text(this.params.keyFileName); this.$el.addClass('open--key-file'); this.inputEl.focus(); }, @@ -168,54 +145,23 @@ var OpenView = Backbone.View.extend({ }).bind(this)); }, - createDemo: function() { - if (!this.file.get('opening')) { - if (!this.model.files.getByName('Demo')) { - this.file.createDemo(); - } else { - this.trigger('cancel'); - } - } - }, - - createNew: function() { - if (!this.file.get('opening')) { - var name; - for (var i = 0; ; i++) { - name = 'New' + (i || ''); - if (!this.model.files.getByName(name)) { - break; - } - } - this.file.create(name); - } - }, - - showOpenLocalFile: function(path) { - if (path && Launcher) { - var that = this; - Storage.file.load(path, function(data, err) { - if (!err) { - var name = path.match(/[^/\\]*$/)[0]; - var file = new Blob([data]); - Object.defineProperties(file, { - path: { value: path }, - name: { value: name } - }); - that.setFile(file); - } - }); - } - }, - showClosedFile: function(file) { - this.setFileModel(file); - this.fileData = file.data; + this.params = { + id: file.get('id'), + name: file.get('name'), + storage: file.get('storage'), + path: file.get('path'), + offline: file.get('offline'), + availOffline: file.get('availOffline'), + keyFileName: null, + keyFileData: null, + fileData: file.data + }; this.displayOpenFile(); }, openFile: function() { - if (!this.file.get('opening')) { + if (!this.busy) { this.openAny('fileData'); } }, @@ -223,10 +169,10 @@ var OpenView = Backbone.View.extend({ openKeyFile: function(e) { if ($(e.target).hasClass('open__settings-key-file-dropbox')) { this.openKeyFileFromDropbox(); - } else if (!this.file.get('opening') && this.file.get('name')) { - if (this.keyFileData) { - this.keyFileData = null; - this.file.set('keyFileName', ''); + } else if (!this.busy && this.params.name) { + if (this.params.keyFileData) { + this.params.keyFileData = null; + this.params.keyFileName = ''; this.$el.removeClass('open--key-file'); this.$el.find('.open__settings-key-file-name').text('key file'); } else { @@ -236,13 +182,13 @@ var OpenView = Backbone.View.extend({ }, openKeyFileFromDropbox: function() { - if (!this.file.get('opening')) { + if (!this.busy) { DropboxLink.chooseFile((function(err, res) { if (err) { return; } - this.keyFileData = res.data; - this.file.set('keyFileName', res.name); + this.params.keyFileData = res.data; + this.params.keyFileName = res.name; this.displayOpenKeyFile(); }).bind(this)); } @@ -250,27 +196,29 @@ var OpenView = Backbone.View.extend({ openAny: function(reading, ext) { this.reading = reading; - this[reading] = null; + this.params[reading] = null; this.$el.find('.open__file-ctrl').attr('accept', ext || '').val(null).click(); }, - openDb: function() { - if (!this.file.get('opening') && this.file.get('name')) { - var offlineChecked = this.$el.find('#open__settings-check-offline').is(':checked'); - if (this.file.get('offline') || - this.file.get('storage') !== 'file' && offlineChecked) { - this.file.set('availOffline', true); - } - var arg = { - password: this.passwordInput.value, - fileData: this.fileData, - keyFileData: this.keyFileData - }; - this.file.set({opening: true, error: false}); - this.afterPaint(function () { - this.file.open(arg.password, arg.fileData, arg.keyFileData); - }); + openLast: function(e) { + if (this.busy) { + return; } + var id = $(e.target).closest('.open__last-item').data('id').toString(); + if ($(e.target).is('.open__last-item-icon-del')) { + this.model.removeFileInfo(id); + this.$el.find('.open__last-item[data-id="' + id + '"]').remove(); + return; + } + //var lastOpenFile = LastOpenFiles.byName(name); + //switch (lastOpenFile.storage) { + // case 'dropbox': + // return this.openDropboxFile(lastOpenFile.path); + // case 'file': + // return this.showOpenLocalFile(lastOpenFile.path); + // default: + // return this.openCache(name); + //} }, inputKeydown: function(e) { @@ -298,145 +246,6 @@ var OpenView = Backbone.View.extend({ this.$el.find('.open__file-warning').toggleClass('invisible', on); }, - openFromDropbox: function() { - if (this.dropboxLoading || this.file.get('opening')) { - return; - } - var that = this; - DropboxLink.authenticate(function(err) { - if (err) { - return; - } - that.dropboxLoading = 'file list'; - that.displayDropboxLoading(); - DropboxLink.getFileList(function(err, files, dirStat) { - that.dropboxLoading = null; - that.displayDropboxLoading(); - if (err) { - return; - } - var buttons = []; - var allFileNames = {}; - files.forEach(function(file) { - var fileName = file.replace(/\.kdbx/i, ''); - buttons.push({ result: file, title: fileName }); - allFileNames[fileName] = true; - }); - if (!buttons.length) { - Alerts.error({ - header: 'Nothing found', - body: 'You have no files in your Dropbox which could be opened.' + - (dirStat && dirStat.inAppFolder ? ' Files are searched inside app folder in your Dropbox.' : '') - }); - return; - } - buttons.push({ result: '', title: 'Cancel' }); - Alerts.alert({ - header: 'Select a file', - body: 'Select a file from your Dropbox which you would like to open', - icon: 'dropbox', - buttons: buttons, - esc: '', - click: '', - success: that.openDropboxFile.bind(that), - cancel: function() { - that.dropboxLoading = null; - that.displayDropboxLoading(); - } - }); - LastOpenFiles.all().forEach(function(lastOpenFile) { - if (lastOpenFile.storage === 'dropbox' && !allFileNames[lastOpenFile.name]) { - that.delLast(lastOpenFile.name); - } - }); - }); - }); - }, - - openDropboxFile: function(file) { - var fileName = file.replace(/\.kdbx/i, ''); - this.dropboxLoading = fileName; - this.displayDropboxLoading(); - var lastOpen = LastOpenFiles.byName(fileName); - var errorAlertCallback = lastOpen && lastOpen.storage === 'dropbox' && lastOpen.availOffline ? - this.dropboxErrorCallback.bind(this, fileName) : null; - DropboxLink.openFile(file, (function(err, data) { - this.dropboxLoading = null; - this.displayDropboxLoading(); - if (err || !data || !data.size) { - return; - } - Object.defineProperties(data, { - storage: { value: 'dropbox' }, - path: { value: file }, - name: { value: fileName } - }); - this.setFile(data); - }).bind(this), errorAlertCallback); - }, - - dropboxErrorCallback: function(fileName, alertConfig) { - alertConfig.body += '
You have offline version of this file cached. Would you like to open it?'; - alertConfig.buttons = [ - {result: 'offline', title: 'Open offline file'}, - {result: 'yes', title: 'OK'} - ]; - alertConfig.success = (function(result) { - if (result === 'offline') { - this.openCache(fileName, 'dropbox'); - } - }).bind(this); - Alerts.error(alertConfig); - }, - - displayDropboxLoading: function() { - this.$el.find('.open__icon-dropbox .open__icon-i').toggleClass('flip3d', !!this.dropboxLoading); - }, - - openLast: function(e) { - if (this.dropboxLoading || this.file.get('opening')) { - return; - } - var name = $(e.target).closest('.open__last-item').data('name').toString(); - if ($(e.target).is('.open__last-item-icon-del')) { - this.delLast(name); - return; - } - var lastOpenFile = LastOpenFiles.byName(name); - switch (lastOpenFile.storage) { - case 'dropbox': - return this.openDropboxFile(lastOpenFile.path); - case 'file': - return this.showOpenLocalFile(lastOpenFile.path); - default: - return this.openCache(name); - } - }, - - openCache: function(name, storage) { - Storage.cache.load(name, (function(data, err) { - if (err) { - this.delLast(name); - Alerts.error({ - header: 'Error loading file', - body: 'There was an error loading offline file ' + name + '. Please, open it from file' - }); - } else { - this.fileData = data; - this.file.set({ name: name, offline: true, availOffline: true }); - if (storage) { - this.file.set({ storage: storage }); - } - this.displayOpenFile(); - } - }).bind(this)); - }, - - delLast: function(name) { - LastOpenFiles.remove(name); - this.$el.find('.open__last-item[data-name="' + name + '"]').remove(); - }, - dragover: function(e) { e.preventDefault(); if (this.dragTimeout) { @@ -468,6 +277,162 @@ var OpenView = Backbone.View.extend({ if (dataFile) { this.setFile(dataFile, keyFile); } + }, + + displayDropboxLoading: function(isLoading) { + this.$el.find('.open__icon-dropbox .open__icon-i').toggleClass('flip3d', !!isLoading); + }, + + openFromDropbox: function() { + if (this.busy) { + return; + } + var that = this; + DropboxLink.authenticate(function(err) { + if (err) { + return; + } + that.busy = true; + that.displayDropboxLoading(true); + DropboxLink.getFileList(function(err, files, dirStat) { + that.busy = false; + that.displayDropboxLoading(false); + if (err) { + return; + } + var buttons = []; + var allFileNames = {}; + files.forEach(function(file) { + var fileName = file.replace(/\.kdbx/i, ''); + buttons.push({ result: file, title: fileName }); + allFileNames[fileName] = true; + }); + if (!buttons.length) { + Alerts.error({ + header: 'Nothing found', + body: 'You have no files in your Dropbox which could be opened.' + + (dirStat && dirStat.inAppFolder ? ' Files are searched inside app folder in your Dropbox.' : '') + }); + return; + } + buttons.push({ result: '', title: 'Cancel' }); + Alerts.alert({ + header: 'Select a file', + body: 'Select a file from your Dropbox which you would like to open', + icon: 'dropbox', + buttons: buttons, + esc: '', + click: '', + success: that.openDropboxFile.bind(that) + }); + that.model.fileInfos.forEach(function(fi) { + if (fi.get('storage') === 'dropbox' && !fi.get('modified') && !allFileNames[fi.get('name')]) { + that.model.removeFileInfo(fi.get('id')); + } + }); + }); + }); + }, + + openDropboxFile: function(file) { + //var fileName = file.replace(/\.kdbx/i, ''); + //this.busy = true; + //var lastOpen = LastOpenFiles.byName(fileName); + //var errorAlertCallback = lastOpen && lastOpen.storage === 'dropbox' && lastOpen.availOffline ? + // this.dropboxErrorCallback.bind(this, fileName) : null; + //DropboxLink.openFile(file, (function(err, data) { + // this.busy = false; + // if (err || !data || !data.size) { + // return; + // } + // Object.defineProperties(data, { + // storage: { value: 'dropbox' }, + // path: { value: file }, + // name: { value: fileName } + // }); + // this.setFile(data); + //}).bind(this), errorAlertCallback); + }, + + showOpenFileInfo: function() { + }, + + showOpenLocalFile: function(path) { + //if (path && Launcher) { + // var that = this; + // Storage.file.load(path, function(data, err) { + // if (!err) { + // var name = path.match(/[^/\\]*$/)[0]; + // var file = new Blob([data]); + // Object.defineProperties(file, { + // path: { value: path }, + // name: { value: name } + // }); + // that.setFile(file); + // } + // }); + //} + }, + + openCache: function(name, storage) { + //Storage.cache.load(name, (function(data, err) { + // if (err) { + // this.delLast(name); + // Alerts.error({ + // header: 'Error loading file', + // body: 'There was an error loading offline file ' + name + '. Please, open it from file' + // }); + // } else { + // this.fileData = data; + // this.file.set({ name: name, offline: true, availOffline: true }); + // if (storage) { + // this.file.set({ storage: storage }); + // } + // this.displayOpenFile(); + // } + //}).bind(this)); + }, + + createDemo: function() { + if (!this.busy) { + if (!this.model.createDemoFile()) { + this.trigger('close'); + } + } + }, + + createNew: function() { + if (!this.busy) { + this.model.createNewFile(); + } + }, + + openDb: function() { + if (!this.busy) { + var offlineChecked = this.$el.find('#open__settings-check-offline').is(':checked'); + this.params.availOffline = this.params.offline || + this.params.storage !== 'file' && offlineChecked; + this.$el.toggleClass('open--opening', true); + this.inputEl.attr('disabled', 'disabled'); + this.$el.find('#open__settings-check-offline').attr('disabled', 'disabled'); + this.busy = true; + this.params.password = this.passwordInput.value; + this.afterPaint(this.model.openFile.bind(this.model, this.params, this.openDbComplete.bind(this))); + } + }, + + openDbComplete: function(err) { + this.busy = false; + this.$el.toggleClass('open--opening', false); + this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err); + this.$el.find('#open__settings-check-offline').removeAttr('disabled'); + if (err) { + this.inputEl.focus(); + this.inputEl[0].selectionStart = 0; + this.inputEl[0].selectionEnd = this.inputEl.val().length; + } else { + this.trigger('close'); + } } }); diff --git a/app/templates/open.html b/app/templates/open.html index 9aaef660..a7c8defd 100644 --- a/app/templates/open.html +++ b/app/templates/open.html @@ -42,7 +42,7 @@
<% lastOpenFiles.forEach(function(file) { %> -
+
<%- file.name %> From 530596b6150c6f3ce040977478e0f6c6445f3a6f Mon Sep 17 00:00:00 2001 From: Antelle Date: Mon, 7 Dec 2015 01:13:29 +0300 Subject: [PATCH 16/49] save uuid --- app/scripts/models/app-model.js | 16 +++++++++------- app/scripts/storage/storage-file-cache.js | 2 -- app/scripts/util/id-generator.js | 13 +++++++++++++ app/scripts/views/open-view.js | 2 +- 4 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 app/scripts/util/id-generator.js diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index c34cedaf..c3d3ed0e 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -10,7 +10,8 @@ var Backbone = require('backbone'), FileInfoCollection = require('../collections/file-info-collection'), FileModel = require('./file-model'), FileInfoModel = require('./file-info-model'), - Storage = require('../storage'); + Storage = require('../storage'), + IdGenerator = require('../util/id-generator'); var AppModel = Backbone.Model.extend({ defaults: {}, @@ -260,11 +261,12 @@ var AppModel = Backbone.Model.extend({ } if (!params.offline) { if (params.availOffline) { - Storage.cache.save(file.id, params.fileData, function(err) { + var cacheId = IdGenerator.uuid(); + Storage.cache.save(cacheId, params.fileData, function(err) { if (err) { file.set('availOffline', false); } - that.addToLastOpenFiles(file); + that.addToLastOpenFiles(file, cacheId); }); } else { Storage.cache.remove(file.id); @@ -274,9 +276,9 @@ var AppModel = Backbone.Model.extend({ }); }, - addToLastOpenFiles: function(file) { + addToLastOpenFiles: function(file, id) { var fileInfo = new FileInfoModel({ - id: file.id, + id: id, name: file.get('name'), storage: file.get('storage'), path: file.get('path'), @@ -287,8 +289,8 @@ var AppModel = Backbone.Model.extend({ pullDate: null, openDate: new Date() }); - this.fileInfos.remove(file.id); - this.fileInfos.push(fileInfo); + this.fileInfos.remove(id); + this.fileInfos.unshift(fileInfo); this.fileInfos.save(); }, diff --git a/app/scripts/storage/storage-file-cache.js b/app/scripts/storage/storage-file-cache.js index 9a3cb1e9..bec6af9b 100644 --- a/app/scripts/storage/storage-file-cache.js +++ b/app/scripts/storage/storage-file-cache.js @@ -9,8 +9,6 @@ var StorageFileCache = { path: null, getPath: function(id) { - // get safe file name by base64 as described in RFC3548: http://tools.ietf.org/html/rfc3548#page-6 - id = id.replace(/\//g, '_').replace(/\+/g, '-'); return Launcher.req('path').join(this.path, id); }, diff --git a/app/scripts/util/id-generator.js b/app/scripts/util/id-generator.js new file mode 100644 index 00000000..7c6303a0 --- /dev/null +++ b/app/scripts/util/id-generator.js @@ -0,0 +1,13 @@ +'use strict'; + +var IdGenerator = { + uuid: function() { + var s4 = IdGenerator.s4; + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); + }, + s4: function() { + return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + } +}; + +module.exports = IdGenerator; diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index 5e848870..d18bbf60 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -327,7 +327,7 @@ var OpenView = Backbone.View.extend({ }); that.model.fileInfos.forEach(function(fi) { if (fi.get('storage') === 'dropbox' && !fi.get('modified') && !allFileNames[fi.get('name')]) { - that.model.removeFileInfo(fi.get('id')); + that.model.removeFileInfo(fi.id); } }); }); From acf5bd981306f6c13aa9d5abe93b165ca41c8c65 Mon Sep 17 00:00:00 2001 From: Antelle Date: Mon, 7 Dec 2015 20:31:09 +0300 Subject: [PATCH 17/49] fix #60: tray click event --- electron/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/app.js b/electron/app.js index 1b4fd90a..422086b1 100644 --- a/electron/app.js +++ b/electron/app.js @@ -67,7 +67,7 @@ app.minimizeApp = function() { mainWindow.minimize(); mainWindow.setSkipTaskbar(true); appIcon = new Tray(path.join(__dirname, 'icon.png')); - appIcon.on('clicked', restoreMainWindow); + appIcon.on('click', restoreMainWindow); var contextMenu = Menu.buildFromTemplate([ { label: 'Open KeeWeb', click: restoreMainWindow }, { label: 'Quit KeeWeb', click: closeMainWindow } From 917c4fe6bfeca272dd0e79ebf06d4a1a2cb3577f Mon Sep 17 00:00:00 2001 From: Antelle Date: Mon, 7 Dec 2015 22:07:56 +0300 Subject: [PATCH 18/49] refactor file open --- .../collections/file-info-collection.js | 8 + app/scripts/comp/dropbox-link.js | 4 +- app/scripts/models/app-model.js | 100 +++++++++---- app/scripts/models/file-model.js | 5 +- app/scripts/storage/storage-dropbox.js | 4 +- app/scripts/views/open-view.js | 141 ++++++++---------- 6 files changed, 146 insertions(+), 116 deletions(-) diff --git a/app/scripts/collections/file-info-collection.js b/app/scripts/collections/file-info-collection.js index d997545c..f00e061e 100644 --- a/app/scripts/collections/file-info-collection.js +++ b/app/scripts/collections/file-info-collection.js @@ -23,6 +23,14 @@ var FileInfoCollection = Backbone.Collection.extend({ getLast: function () { this.max(function(file) { return file.get('openDate'); }); + }, + + getMatch: function (storage, name, path) { + return this.find(function(fi) { + return (fi.get('storage') || '') === (storage || '') && + (fi.get('name') || '') === (name || '') && + (fi.get('path') || '') === (path || ''); + }); } }); diff --git a/app/scripts/comp/dropbox-link.js b/app/scripts/comp/dropbox-link.js index c41cd0ff..009790cc 100644 --- a/app/scripts/comp/dropbox-link.js +++ b/app/scripts/comp/dropbox-link.js @@ -257,11 +257,11 @@ var DropboxLink = { }, getFileList: function(complete) { - this._callAndHandleError('readdir', [''], function(err, files, dirStat) { + this._callAndHandleError('readdir', [''], function(err, files, dirStat, filesStat) { if (files) { files = files.filter(function(f) { return /\.kdbx$/i.test(f); }); } - complete(err, files, dirStat); + complete(err, files, dirStat, filesStat); }); }, diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index c3d3ed0e..44da21da 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -41,7 +41,7 @@ var AppModel = Backbone.Model.extend({ file.get('groups').forEach(function (group) { this.menu.groupsSection.addItem(group); }, this); - this._addTags(file.db); + this._addTags(file); this._tagsChanged(); this.menu.filesSection.addItem({ icon: 'lock', @@ -62,22 +62,20 @@ var AppModel = Backbone.Model.extend({ this.updateTags(); }, - _addTags: function(group) { + _addTags: function(file) { var tagsHash = {}; this.tags.forEach(function(tag) { tagsHash[tag.toLowerCase()] = true; }); - _.forEach(group.entries, function(entry) { + var that = this; + file.forEachEntry({}, function(entry) { _.forEach(entry.tags, function(tag) { if (!tagsHash[tag.toLowerCase()]) { tagsHash[tag.toLowerCase()] = true; - this.tags.push(tag); + that.tags.push(tag); } - }, this); - }, this); - _.forEach(group.groups, function(subGroup) { - this._addTags(subGroup); - }, this); + }); + }); this.tags.sort(); }, @@ -97,7 +95,7 @@ var AppModel = Backbone.Model.extend({ var oldTags = this.tags.slice(); this.tags.splice(0, this.tags.length); this.files.forEach(function(file) { - this._addTags(file.db); + this._addTags(file); }, this); if (!_.isEqual(oldTags, this.tags)) { this._tagsChanged(); @@ -248,6 +246,47 @@ var AppModel = Backbone.Model.extend({ openFile: function(params, callback) { var that = this; + var fileInfo = params.id ? this.fileInfos.get(params.id) : this.fileInfos.getMatch(params.storage, params.name, params.path); + if (fileInfo && fileInfo.get('availOffline') && fileInfo.get('modified')) { + // modified offline, cannot overwrite: load from cache + this.openFileFromCache(params, callback, fileInfo); + } else if (params.fileData) { + // has user content: load it + this.openFileWithData(params, callback, fileInfo, params.fileData, true); + } else if (fileInfo && fileInfo.get('availOffline') && fileInfo.rev === params.rev) { + // already latest in cache: use it + this.openFileFromCache(params, callback, fileInfo); + } else { + // try to load from storage and update cache + Storage[params.storage].load(params.path, function(err, data, rev) { + if (err) { + // failed to load from storage: fallback to cache if we can + if (fileInfo && fileInfo.get('availOffline')) { + that.openFileFromCache(params, callback, fileInfo); + } else { + callback(err); + } + } else { + params.fileData = data; + params.rev = rev; + that.openFileWithData(params, callback, fileInfo, data, true); + } + }); + } + }, + + openFileFromCache: function(params, callback, fileInfo) { + var that = this; + Storage.cache.load(fileInfo.id, function(data, err) { + if (err) { + callback(err); + } else { + that.openFileWithData(params, callback, fileInfo, data); + } + }); + }, + + openFileWithData: function(params, callback, fileInfo, data, updateCacheOnSuccess) { var file = new FileModel({ name: params.name, availOffline: params.availOffline, @@ -255,28 +294,33 @@ var AppModel = Backbone.Model.extend({ path: params.path, keyFileName: params.keyFileName }); - file.open(params.password, params.fileData, params.keyFileData, function(err) { - if (err || that.files.get(file.id)) { + var that = this; + file.open(params.password, data, params.keyFileData, function(err) { + if (err) { return callback(err); } - if (!params.offline) { - if (params.availOffline) { - var cacheId = IdGenerator.uuid(); - Storage.cache.save(cacheId, params.fileData, function(err) { - if (err) { - file.set('availOffline', false); - } - that.addToLastOpenFiles(file, cacheId); - }); - } else { - Storage.cache.remove(file.id); - } + if (that.files.get(file.id)) { + return callback('Duplicate file id'); + } + if (params.availOffline && updateCacheOnSuccess) { + var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); + Storage.cache.save(cacheId, params.fileData, function(err) { + if (err) { + file.set('availOffline', false); + if (!params.storage) { return; } + } + that.addToLastOpenFiles(file, cacheId, params.rev); + }); + } + if (!params.availOffline && fileInfo && !fileInfo.get('modified')) { + that.removeFileInfo(fileInfo.id); } that.addFile(file); }); }, - addToLastOpenFiles: function(file, id) { + addToLastOpenFiles: function(file, id, rev) { + var dt = new Date(); var fileInfo = new FileInfoModel({ id: id, name: file.get('name'), @@ -285,9 +329,9 @@ var AppModel = Backbone.Model.extend({ availOffline: file.get('availOffline'), modified: file.get('modified'), editState: null, - pullRev: null, - pullDate: null, - openDate: new Date() + pullRev: rev, + pullDate: dt, + openDate: dt }); this.fileInfos.remove(id); this.fileInfos.unshift(fileInfo); diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 251d0987..4ba764c2 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -26,8 +26,7 @@ var FileModel = Backbone.Model.extend({ passwordChanged: false, keyFileChanged: false, syncing: false, - availOffline: false, - offline: false + availOffline: false }, db: null, @@ -85,7 +84,7 @@ var FileModel = Backbone.Model.extend({ var credentials = new kdbxweb.Credentials(password); this.db = kdbxweb.Kdbx.create(credentials, name); this.readModel(); - this.set({ open: true, created: true, name: name, offline: false }); + this.set({ open: true, created: true, name: name }); }, openDemo: function(callback) { diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js index 77e7bad0..a32b8569 100644 --- a/app/scripts/storage/storage-dropbox.js +++ b/app/scripts/storage/storage-dropbox.js @@ -7,7 +7,9 @@ var StorageDropbox = { enabled: true, load: function(path, callback) { - DropboxLink.openUploadedFile(path, callback); + DropboxLink.openFile(path, function(err, data, stat) { + callback(err, data, stat ? stat.versionTag : null); + }); }, save: function(path, data, callback) { diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index d18bbf60..f60e1522 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -37,11 +37,11 @@ var OpenView = Backbone.View.extend({ name: '', storage: null, path: null, - offline: false, availOffline: false, keyFileName: null, keyFileData: null, - fileData: null + fileData: null, + rev: null }; this.passwordInput = new SecureInput(); }, @@ -93,15 +93,17 @@ var OpenView = Backbone.View.extend({ processFile: function(file, complete) { var reader = new FileReader(); reader.onload = (function(e) { - this.params[this.reading] = e.target.result; if (this.reading === 'fileData') { - _.extend(this.params, { name: file.name.replace(/\.\w+$/i, ''), offline: false }); - if (file.path) { - _.extend(this.params, { path: file.path, storage: file.storage || 'file' }); - } + this.params.id = null; + this.params.fileData = e.target.result; + this.params.name = file.name.replace(/\.\w+$/i, ''); + this.params.path = file.path || null; + this.params.storage = file.path ? 'file' : null; + this.params.rev = null; this.displayOpenFile(); } else { - _.extend(this.params, { keyFileName: file.name }); + this.params.keyFileData = e.target.result; + this.params.keyFileName = file.name; this.displayOpenKeyFile(); } if (complete) { @@ -121,9 +123,8 @@ var OpenView = Backbone.View.extend({ this.$el.addClass('open--file'); this.$el.find('.open__settings-key-file').removeClass('hide'); this.$el.find('#open__settings-check-offline')[0].removeAttribute('disabled'); - var canSwitchOffline = this.params.storage !== 'file' && !this.params.offline; + var canSwitchOffline = this.params.storage !== 'file'; this.$el.find('.open__settings-offline').toggleClass('hide', !canSwitchOffline); - this.$el.find('.open__settings-offline-warning').toggleClass('hide', !this.params.offline); this.inputEl[0].removeAttribute('readonly'); this.inputEl[0].setAttribute('placeholder', 'Password for ' + this.params.name); this.inputEl.focus(); @@ -151,7 +152,6 @@ var OpenView = Backbone.View.extend({ name: file.get('name'), storage: file.get('storage'), path: file.get('path'), - offline: file.get('offline'), availOffline: file.get('availOffline'), keyFileName: null, keyFileData: null, @@ -210,15 +210,7 @@ var OpenView = Backbone.View.extend({ this.$el.find('.open__last-item[data-id="' + id + '"]').remove(); return; } - //var lastOpenFile = LastOpenFiles.byName(name); - //switch (lastOpenFile.storage) { - // case 'dropbox': - // return this.openDropboxFile(lastOpenFile.path); - // case 'file': - // return this.showOpenLocalFile(lastOpenFile.path); - // default: - // return this.openCache(name); - //} + this.showOpenFileInfo(this.model.fileInfos.get(id)); }, inputKeydown: function(e) { @@ -294,18 +286,20 @@ var OpenView = Backbone.View.extend({ } that.busy = true; that.displayDropboxLoading(true); - DropboxLink.getFileList(function(err, files, dirStat) { + DropboxLink.getFileList(function(err, files, dirStat, filesStat) { that.busy = false; that.displayDropboxLoading(false); if (err) { return; } var buttons = []; - var allFileNames = {}; - files.forEach(function(file) { - var fileName = file.replace(/\.kdbx/i, ''); - buttons.push({ result: file, title: fileName }); - allFileNames[fileName] = true; + var allDropboxFiles = {}; + filesStat.forEach(function(file) { + if (!file.isFolder && !file.isRemoved) { + var fileName = file.name.replace(/\.kdbx/i, ''); + buttons.push({ result: file.name, title: fileName }); + allDropboxFiles[file.name] = file; + } }); if (!buttons.length) { Alerts.error({ @@ -323,10 +317,12 @@ var OpenView = Backbone.View.extend({ buttons: buttons, esc: '', click: '', - success: that.openDropboxFile.bind(that) + success: function(file) { + that.openDropboxFile(allDropboxFiles[file]); + } }); that.model.fileInfos.forEach(function(fi) { - if (fi.get('storage') === 'dropbox' && !fi.get('modified') && !allFileNames[fi.get('name')]) { + if (fi.get('storage') === 'dropbox' && !fi.get('modified') && !allDropboxFiles[fi.get('name')]) { that.model.removeFileInfo(fi.id); } }); @@ -334,63 +330,45 @@ var OpenView = Backbone.View.extend({ }); }, - openDropboxFile: function(file) { - //var fileName = file.replace(/\.kdbx/i, ''); - //this.busy = true; - //var lastOpen = LastOpenFiles.byName(fileName); - //var errorAlertCallback = lastOpen && lastOpen.storage === 'dropbox' && lastOpen.availOffline ? - // this.dropboxErrorCallback.bind(this, fileName) : null; - //DropboxLink.openFile(file, (function(err, data) { - // this.busy = false; - // if (err || !data || !data.size) { - // return; - // } - // Object.defineProperties(data, { - // storage: { value: 'dropbox' }, - // path: { value: file }, - // name: { value: fileName } - // }); - // this.setFile(data); - //}).bind(this), errorAlertCallback); + openDropboxFile: function(fileStat) { + if (this.busy) { + return; + } + this.params.id = null; + this.params.storage = 'dropbox'; + this.params.path = fileStat.path; + this.params.name = fileStat.name.replace(/\.kdbx/i, ''); + this.params.rev = fileStat.versionTag; + this.params.fileData = null; + this.displayOpenFile(); }, - showOpenFileInfo: function() { + showOpenFileInfo: function(fileInfo) { + if (this.busy || !fileInfo) { + return; + } + this.params.id = fileInfo.id; + this.params.availOffline = fileInfo.get('availOffline'); + this.params.storage = fileInfo.get('storage'); + this.params.path = fileInfo.get('path'); + this.params.name = fileInfo.get('name'); + this.params.fileData = null; + this.params.rev = null; + this.$el.find('#open__settings-check-offline').prop('checked', this.params.availOffline); + this.displayOpenFile(); }, showOpenLocalFile: function(path) { - //if (path && Launcher) { - // var that = this; - // Storage.file.load(path, function(data, err) { - // if (!err) { - // var name = path.match(/[^/\\]*$/)[0]; - // var file = new Blob([data]); - // Object.defineProperties(file, { - // path: { value: path }, - // name: { value: name } - // }); - // that.setFile(file); - // } - // }); - //} - }, - - openCache: function(name, storage) { - //Storage.cache.load(name, (function(data, err) { - // if (err) { - // this.delLast(name); - // Alerts.error({ - // header: 'Error loading file', - // body: 'There was an error loading offline file ' + name + '. Please, open it from file' - // }); - // } else { - // this.fileData = data; - // this.file.set({ name: name, offline: true, availOffline: true }); - // if (storage) { - // this.file.set({ storage: storage }); - // } - // this.displayOpenFile(); - // } - //}).bind(this)); + if (this.busy) { + return; + } + this.params.id = null; + this.params.storage = 'file'; + this.params.path = path; + this.params.name = path.match(/[^/\\]*$/)[0]; + this.params.rev = null; + this.params.fileData = null; + this.displayOpenFile(); }, createDemo: function() { @@ -410,8 +388,7 @@ var OpenView = Backbone.View.extend({ openDb: function() { if (!this.busy) { var offlineChecked = this.$el.find('#open__settings-check-offline').is(':checked'); - this.params.availOffline = this.params.offline || - this.params.storage !== 'file' && offlineChecked; + this.params.availOffline = this.params.storage !== 'file' && offlineChecked; this.$el.toggleClass('open--opening', true); this.inputEl.attr('disabled', 'disabled'); this.$el.find('#open__settings-check-offline').attr('disabled', 'disabled'); From 2d5fb037fd23d95c5b3fbbd4fc445a76e4c4ea99 Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 00:00:44 +0300 Subject: [PATCH 19/49] file open bugfixes --- app/scripts/comp/dropbox-link.js | 2 +- app/scripts/models/app-model.js | 6 +-- app/scripts/models/file-info-model.js | 2 +- app/scripts/storage/storage-cache.js | 44 +++++++------------ app/scripts/storage/storage-dropbox.js | 4 +- app/scripts/storage/storage-file-cache.js | 52 ++++++++++------------- app/scripts/storage/storage-file.js | 8 ++-- app/scripts/views/app-view.js | 5 ++- app/scripts/views/open-view.js | 20 ++------- 9 files changed, 56 insertions(+), 87 deletions(-) diff --git a/app/scripts/comp/dropbox-link.js b/app/scripts/comp/dropbox-link.js index 009790cc..97090dc4 100644 --- a/app/scripts/comp/dropbox-link.js +++ b/app/scripts/comp/dropbox-link.js @@ -253,7 +253,7 @@ var DropboxLink = { }, openFile: function(fileName, complete, errorAlertCallback) { - this._callAndHandleError('readFile', [fileName, { blob: true }], complete, errorAlertCallback); + this._callAndHandleError('readFile', [fileName, { arrayBuffer: true }], complete, errorAlertCallback); }, getFileList: function(complete) { diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 44da21da..c6c86f98 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -253,7 +253,7 @@ var AppModel = Backbone.Model.extend({ } else if (params.fileData) { // has user content: load it this.openFileWithData(params, callback, fileInfo, params.fileData, true); - } else if (fileInfo && fileInfo.get('availOffline') && fileInfo.rev === params.rev) { + } else if (fileInfo && fileInfo.get('availOffline') && fileInfo.get('rev') === params.rev) { // already latest in cache: use it this.openFileFromCache(params, callback, fileInfo); } else { @@ -277,7 +277,7 @@ var AppModel = Backbone.Model.extend({ openFileFromCache: function(params, callback, fileInfo) { var that = this; - Storage.cache.load(fileInfo.id, function(data, err) { + Storage.cache.load(fileInfo.id, function(err, data) { if (err) { callback(err); } else { @@ -329,7 +329,7 @@ var AppModel = Backbone.Model.extend({ availOffline: file.get('availOffline'), modified: file.get('modified'), editState: null, - pullRev: rev, + rev: rev, pullDate: dt, openDate: dt }); diff --git a/app/scripts/models/file-info-model.js b/app/scripts/models/file-info-model.js index b165d3e4..e56abbdb 100644 --- a/app/scripts/models/file-info-model.js +++ b/app/scripts/models/file-info-model.js @@ -11,7 +11,7 @@ var FileInfoModel = Backbone.Model.extend({ availOffline: false, modified: false, editState: null, - pullRev: null, + rev: null, pullDate: null, openDate: null }, diff --git a/app/scripts/storage/storage-cache.js b/app/scripts/storage/storage-cache.js index 42e3405a..8405decf 100644 --- a/app/scripts/storage/storage-cache.js +++ b/app/scripts/storage/storage-cache.js @@ -11,7 +11,7 @@ var StorageCache = { init: function(callback) { if (this.db) { - return callback(); + return callback && callback(); } var that = this; try { @@ -19,11 +19,11 @@ var StorageCache = { req.onerror = function (e) { console.error('Error opening indexed db', e); that.errorOpening = e; - callback(e); + if (callback) { callback(e); } }; req.onsuccess = function (e) { that.db = e.target.result; - callback(); + if (callback) { callback(); } }; req.onupgradeneeded = function (e) { var db = e.target.result; @@ -31,31 +31,27 @@ var StorageCache = { }; } catch (e) { console.error('Error opening indexed db', e); - callback(e); + if (callback) { callback(e); } } }, save: function(id, data, callback) { this.init((function(err) { if (err) { - return callback(err); + return callback && callback(err); } try { var req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id); req.onsuccess = function () { - if (callback) { - callback(); - } + if (callback) { callback(); } }; req.onerror = function () { console.error('Error saving to cache', id, req.error); - if (callback) { - callback(req.error); - } + if (callback) { callback(req.error); } }; } catch (e) { console.error('Error saving to cache', id, e); - callback(e); + if (callback) { callback(e); } } }).bind(this)); }, @@ -63,24 +59,20 @@ var StorageCache = { load: function(id, callback) { this.init((function(err) { if (err) { - return callback(null, err); + return callback && callback(err, null); } try { var req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id); req.onsuccess = function () { - if (callback) { - callback(req.result); - } + if (callback) { callback(null, req.result); } }; req.onerror = function () { console.error('Error loading from cache', id, req.error); - if (callback) { - callback(null, req.error); - } + if (callback) { callback(req.error); } }; } catch (e) { console.error('Error loading from cache', id, e); - callback(null, e); + if (callback) { callback(e, null); } } }).bind(this)); }, @@ -88,24 +80,20 @@ var StorageCache = { remove: function(id, callback) { this.init((function(err) { if (err) { - return callback(err); + return callback && callback(err); } try { var req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id); req.onsuccess = function () { - if (callback) { - callback(); - } + if (callback) { callback(); } }; req.onerror = function () { console.error('Error removing from cache', id, req.error); - if (callback) { - callback(req.error); - } + if (callback) { callback(req.error); } }; } catch(e) { console.error('Error removing from cache', id, e); - callback(e); + if (callback) { callback(e); } } }).bind(this)); } diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js index a32b8569..0afbf39e 100644 --- a/app/scripts/storage/storage-dropbox.js +++ b/app/scripts/storage/storage-dropbox.js @@ -8,12 +8,12 @@ var StorageDropbox = { load: function(path, callback) { DropboxLink.openFile(path, function(err, data, stat) { - callback(err, data, stat ? stat.versionTag : null); + if (callback) { callback(err, data, stat ? stat.versionTag : null); } }); }, save: function(path, data, callback) { - DropboxLink.saveFile(path, data, true, callback); + DropboxLink.saveFile(path, data, true, callback || _.noop); } }; diff --git a/app/scripts/storage/storage-file-cache.js b/app/scripts/storage/storage-file-cache.js index bec6af9b..d8809c5a 100644 --- a/app/scripts/storage/storage-file-cache.js +++ b/app/scripts/storage/storage-file-cache.js @@ -14,36 +14,32 @@ var StorageFileCache = { init: function(callback) { if (this.path) { - return callback(); + return callback && callback(); } - if (Launcher) { - try { - var path = Launcher.getUserDataPath('OfflineFiles'); - var fs = Launcher.req('fs'); - if (!fs.existsSync(path)) { - fs.mkdirSync(path); - } - this.path = path; - } catch (e) { - console.error('Error opening local offline storage', e); - callback(e); + try { + var path = Launcher.getUserDataPath('OfflineFiles'); + var fs = Launcher.req('fs'); + if (!fs.existsSync(path)) { + fs.mkdirSync(path); } + this.path = path; + } catch (e) { + console.error('Error opening local offline storage', e); + if (callback) { callback(e); } } }, save: function(id, data, callback) { this.init((function(err) { if (err) { - return callback(err); + return callback && callback(err); } try { - if (Launcher) { - Launcher.writeFile(this.getPath(id), data); - return callback(); - } + Launcher.writeFile(this.getPath(id), data); + if (callback) { callback(); } } catch (e) { console.error('Error saving to cache', id, e); - callback(e); + if (callback) { callback(e); } } }).bind(this)); }, @@ -51,16 +47,14 @@ var StorageFileCache = { load: function(id, callback) { this.init((function(err) { if (err) { - return callback(null, err); + return callback && callback(null, err); } try { - if (Launcher) { - var data = Launcher.readFile(this.getPath(id)); - return callback(data.buffer); - } + var data = Launcher.readFile(this.getPath(id)); + if (callback) { callback(null, data.buffer); } } catch (e) { console.error('Error loading from cache', id, e); - callback(null, e); + if (callback) { callback(e, null); } } }).bind(this)); }, @@ -68,16 +62,14 @@ var StorageFileCache = { remove: function(id, callback) { this.init((function(err) { if (err) { - return callback(err); + return callback && callback(err); } try { - if (Launcher) { - Launcher.deleteFile(this.getPath(id)); - return callback(); - } + Launcher.deleteFile(this.getPath(id)); + if (callback) { callback(); } } catch(e) { console.error('Error removing from cache', id, e); - callback(e); + if (callback) { callback(e); } } }).bind(this)); } diff --git a/app/scripts/storage/storage-file.js b/app/scripts/storage/storage-file.js index 841f2327..b0256f2d 100644 --- a/app/scripts/storage/storage-file.js +++ b/app/scripts/storage/storage-file.js @@ -9,20 +9,20 @@ var StorageFile = { load: function(path, callback) { try { var data = Launcher.readFile(path); - callback(data.buffer); + if (callback) { callback(null, data.buffer); } } catch (e) { console.error('Error reading local file', path, e); - callback(null, e); + if (callback) { callback(e, null); } } }, save: function(path, data, callback) { try { Launcher.writeFile(path, data); - callback(); + if (callback) { callback(); } } catch (e) { console.error('Error writing local file', path, e); - callback(e); + if (callback) { callback(e); } } } }; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index 91397cfe..f9187c8b 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -377,7 +377,10 @@ var AppView = Backbone.View.extend({ var firstFile = this.model.files.find(function(file) { return !file.get('demo') && !file.get('created'); }); this.model.closeAllFiles(); if (firstFile) { - this.views.open.showClosedFile(firstFile); + var fileInfo = this.model.fileInfos.getMatch(firstFile.get('storage'), firstFile.get('name'), firstFile.get('path')); + if (fileInfo) { + this.views.open.showOpenFileInfo(fileInfo); + } } }, diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index f60e1522..32350292 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -146,20 +146,6 @@ var OpenView = Backbone.View.extend({ }).bind(this)); }, - showClosedFile: function(file) { - this.params = { - id: file.get('id'), - name: file.get('name'), - storage: file.get('storage'), - path: file.get('path'), - availOffline: file.get('availOffline'), - keyFileName: null, - keyFileData: null, - fileData: file.data - }; - this.displayOpenFile(); - }, - openFile: function() { if (!this.busy) { this.openAny('fileData'); @@ -297,8 +283,8 @@ var OpenView = Backbone.View.extend({ filesStat.forEach(function(file) { if (!file.isFolder && !file.isRemoved) { var fileName = file.name.replace(/\.kdbx/i, ''); - buttons.push({ result: file.name, title: fileName }); - allDropboxFiles[file.name] = file; + buttons.push({ result: file.path, title: fileName }); + allDropboxFiles[file.path] = file; } }); if (!buttons.length) { @@ -322,7 +308,7 @@ var OpenView = Backbone.View.extend({ } }); that.model.fileInfos.forEach(function(fi) { - if (fi.get('storage') === 'dropbox' && !fi.get('modified') && !allDropboxFiles[fi.get('name')]) { + if (fi.get('storage') === 'dropbox' && !fi.get('modified') && !allDropboxFiles[fi.get('path')]) { that.model.removeFileInfo(fi.id); } }); From 1227698aebfaee3ba9ea1a138ec384e9ee3bffec Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 00:20:18 +0300 Subject: [PATCH 20/49] bugfixes --- app/scripts/collections/file-info-collection.js | 2 +- app/scripts/models/app-model.js | 5 ++++- app/scripts/storage/storage-file-cache.js | 5 ++++- app/scripts/views/open-view.js | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/scripts/collections/file-info-collection.js b/app/scripts/collections/file-info-collection.js index f00e061e..45e5b505 100644 --- a/app/scripts/collections/file-info-collection.js +++ b/app/scripts/collections/file-info-collection.js @@ -22,7 +22,7 @@ var FileInfoCollection = Backbone.Collection.extend({ }, getLast: function () { - this.max(function(file) { return file.get('openDate'); }); + return this.first(); }, getMatch: function (storage, name, path) { diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index c6c86f98..588f9d00 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -302,8 +302,8 @@ var AppModel = Backbone.Model.extend({ if (that.files.get(file.id)) { return callback('Duplicate file id'); } + var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); if (params.availOffline && updateCacheOnSuccess) { - var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); Storage.cache.save(cacheId, params.fileData, function(err) { if (err) { file.set('availOffline', false); @@ -315,6 +315,9 @@ var AppModel = Backbone.Model.extend({ if (!params.availOffline && fileInfo && !fileInfo.get('modified')) { that.removeFileInfo(fileInfo.id); } + if (params.storage === 'file') { + that.addToLastOpenFiles(file, cacheId, params.rev); + } that.addFile(file); }); }, diff --git a/app/scripts/storage/storage-file-cache.js b/app/scripts/storage/storage-file-cache.js index d8809c5a..dcc57d73 100644 --- a/app/scripts/storage/storage-file-cache.js +++ b/app/scripts/storage/storage-file-cache.js @@ -65,7 +65,10 @@ var StorageFileCache = { return callback && callback(err); } try { - Launcher.deleteFile(this.getPath(id)); + var path = this.getPath(id); + if (Launcher.fileExists(path)) { + Launcher.deleteFile(path); + } if (callback) { callback(); } } catch(e) { console.error('Error removing from cache', id, e); diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index 32350292..d8c7de74 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -59,7 +59,7 @@ var OpenView = Backbone.View.extend({ getLastOpenFiles: function() { return this.model.fileInfos.map(function(f) { var icon; - switch (f.storage) { + switch (f.get('storage')) { case 'dropbox': icon = 'dropbox'; break; From 5f947df27d549d2c943152731435678b87a4af13 Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 08:05:57 +0300 Subject: [PATCH 21/49] completely removed non-offline files --- app/scripts/models/app-model.js | 18 ++++++------------ app/scripts/models/file-info-model.js | 1 - app/scripts/models/file-model.js | 3 +-- app/scripts/views/open-view.js | 10 ---------- app/styles/areas/_open.scss | 8 +------- app/templates/open.html | 5 ----- 6 files changed, 8 insertions(+), 37 deletions(-) diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 588f9d00..8998eb64 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -247,13 +247,13 @@ var AppModel = Backbone.Model.extend({ openFile: function(params, callback) { var that = this; var fileInfo = params.id ? this.fileInfos.get(params.id) : this.fileInfos.getMatch(params.storage, params.name, params.path); - if (fileInfo && fileInfo.get('availOffline') && fileInfo.get('modified')) { + if (fileInfo && fileInfo.get('modified')) { // modified offline, cannot overwrite: load from cache this.openFileFromCache(params, callback, fileInfo); } else if (params.fileData) { // has user content: load it this.openFileWithData(params, callback, fileInfo, params.fileData, true); - } else if (fileInfo && fileInfo.get('availOffline') && fileInfo.get('rev') === params.rev) { + } else if (fileInfo && fileInfo.get('rev') === params.rev) { // already latest in cache: use it this.openFileFromCache(params, callback, fileInfo); } else { @@ -261,7 +261,7 @@ var AppModel = Backbone.Model.extend({ Storage[params.storage].load(params.path, function(err, data, rev) { if (err) { // failed to load from storage: fallback to cache if we can - if (fileInfo && fileInfo.get('availOffline')) { + if (fileInfo) { that.openFileFromCache(params, callback, fileInfo); } else { callback(err); @@ -289,7 +289,6 @@ var AppModel = Backbone.Model.extend({ openFileWithData: function(params, callback, fileInfo, data, updateCacheOnSuccess) { var file = new FileModel({ name: params.name, - availOffline: params.availOffline, storage: params.storage, path: params.path, keyFileName: params.keyFileName @@ -303,18 +302,14 @@ var AppModel = Backbone.Model.extend({ return callback('Duplicate file id'); } var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); - if (params.availOffline && updateCacheOnSuccess) { + if (updateCacheOnSuccess) { Storage.cache.save(cacheId, params.fileData, function(err) { - if (err) { - file.set('availOffline', false); - if (!params.storage) { return; } + if (err && !params.storage) { + return; } that.addToLastOpenFiles(file, cacheId, params.rev); }); } - if (!params.availOffline && fileInfo && !fileInfo.get('modified')) { - that.removeFileInfo(fileInfo.id); - } if (params.storage === 'file') { that.addToLastOpenFiles(file, cacheId, params.rev); } @@ -329,7 +324,6 @@ var AppModel = Backbone.Model.extend({ name: file.get('name'), storage: file.get('storage'), path: file.get('path'), - availOffline: file.get('availOffline'), modified: file.get('modified'), editState: null, rev: rev, diff --git a/app/scripts/models/file-info-model.js b/app/scripts/models/file-info-model.js index e56abbdb..4b75d5d6 100644 --- a/app/scripts/models/file-info-model.js +++ b/app/scripts/models/file-info-model.js @@ -8,7 +8,6 @@ var FileInfoModel = Backbone.Model.extend({ name: '', storage: null, path: null, - availOffline: false, modified: false, editState: null, rev: null, diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 4ba764c2..a873f75f 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -25,8 +25,7 @@ var FileModel = Backbone.Model.extend({ oldKeyFileName: '', passwordChanged: false, keyFileChanged: false, - syncing: false, - availOffline: false + syncing: false }, db: null, diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index d8c7de74..f6292872 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -37,7 +37,6 @@ var OpenView = Backbone.View.extend({ name: '', storage: null, path: null, - availOffline: false, keyFileName: null, keyFileData: null, fileData: null, @@ -122,9 +121,6 @@ var OpenView = Backbone.View.extend({ displayOpenFile: function() { this.$el.addClass('open--file'); this.$el.find('.open__settings-key-file').removeClass('hide'); - this.$el.find('#open__settings-check-offline')[0].removeAttribute('disabled'); - var canSwitchOffline = this.params.storage !== 'file'; - this.$el.find('.open__settings-offline').toggleClass('hide', !canSwitchOffline); this.inputEl[0].removeAttribute('readonly'); this.inputEl[0].setAttribute('placeholder', 'Password for ' + this.params.name); this.inputEl.focus(); @@ -334,13 +330,11 @@ var OpenView = Backbone.View.extend({ return; } this.params.id = fileInfo.id; - this.params.availOffline = fileInfo.get('availOffline'); this.params.storage = fileInfo.get('storage'); this.params.path = fileInfo.get('path'); this.params.name = fileInfo.get('name'); this.params.fileData = null; this.params.rev = null; - this.$el.find('#open__settings-check-offline').prop('checked', this.params.availOffline); this.displayOpenFile(); }, @@ -373,11 +367,8 @@ var OpenView = Backbone.View.extend({ openDb: function() { if (!this.busy) { - var offlineChecked = this.$el.find('#open__settings-check-offline').is(':checked'); - this.params.availOffline = this.params.storage !== 'file' && offlineChecked; this.$el.toggleClass('open--opening', true); this.inputEl.attr('disabled', 'disabled'); - this.$el.find('#open__settings-check-offline').attr('disabled', 'disabled'); this.busy = true; this.params.password = this.passwordInput.value; this.afterPaint(this.model.openFile.bind(this.model, this.params, this.openDbComplete.bind(this))); @@ -388,7 +379,6 @@ var OpenView = Backbone.View.extend({ this.busy = false; this.$el.toggleClass('open--opening', false); this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err); - this.$el.find('#open__settings-check-offline').removeAttr('disabled'); if (err) { this.inputEl.focus(); this.inputEl[0].selectionStart = 0; diff --git a/app/styles/areas/_open.scss b/app/styles/areas/_open.scss index 1f83f164..1fe407f2 100644 --- a/app/styles/areas/_open.scss +++ b/app/styles/areas/_open.scss @@ -103,8 +103,7 @@ } } - &-key-file, &-label-offline, &-label-offline:before, input[type=checkbox] + label.open__settings-label-offline:before, - &-key-file-dropbox { + &-key-file, &-key-file-dropbox { @include th { color: muted-color(); } @@ -114,11 +113,6 @@ } } } - &-label-offline { font-weight: normal; } - } - - &--file:not(.open--opening) input[type=checkbox] + label.open__settings-label-offline:hover:before { - @include th { color: medium-color(); } } &__last { diff --git a/app/templates/open.html b/app/templates/open.html index a7c8defd..220e1761 100644 --- a/app/templates/open.html +++ b/app/templates/open.html @@ -34,11 +34,6 @@ key file (from dropbox)
-
- - -
-
saved offline version
<% lastOpenFiles.forEach(function(file) { %> From 0973c08bcb77c00e5e16a61f49828163273bae6e Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 08:12:53 +0300 Subject: [PATCH 22/49] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 554ddc80..eb6a05c7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ Twitter: [kee_web](https://twitter.com/kee_web) # Status -Reading and display is mostly complete; modification and sync is under construction, please see [TODO](https://github.com/antelle/keeweb/wiki/TODO) for more details. +The app is already rather stable but might still needs polishing, testing ang improvements before v1 release, which is expected to happend in Feb 2016. +Please see [TODO](https://github.com/antelle/keeweb/wiki/TODO) for more details. # Known Issues From b08fa3aed2339a7782831ef235e5fafa3e3f7eeb Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 08:14:14 +0300 Subject: [PATCH 23/49] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb6a05c7..2c32ac15 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Twitter: [kee_web](https://twitter.com/kee_web) # Status -The app is already rather stable but might still needs polishing, testing ang improvements before v1 release, which is expected to happend in Feb 2016. +The app is already rather stable but might still need polishing, testing ang 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. # Known Issues From 5fb1f5715110c474d27a1ea34ac020ec1ffd01ba Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 19:48:39 +0300 Subject: [PATCH 24/49] readme --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2c32ac15..86466e78 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # KeePass web app (unofficial) This webapp is a browser and desktop password manager compatible with KeePass databases. It doesn't require any server or additional resources. -The app can run either in browser, or as a desktop app. +The app can run either in browser, or as a desktop app. ![screenshot](https://habrastorage.org/files/bfb/51e/d8d/bfb51ed8d19847d8afb827c4fbff7dd5.png) @@ -14,7 +14,7 @@ Twitter: [kee_web](https://twitter.com/kee_web) # Status -The app is already rather stable but might still need polishing, testing ang improvements before v1 release, which is expected to happen in Feb 2016. +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. # Known Issues @@ -32,17 +32,17 @@ To make Dropbox work in your self-hosted app: 1. [create](https://www.dropbox.com/developers/apps/create) a Dropbox app 2. find your app key (in Dropbox App page, go to Settings/App key) -3. change Dropbox app key in index.html file: `sed -i.bak s/qp7ctun6qt5n9d6/your_app_key/g index.html` +3. change Dropbox app key in index.html file: `sed -i.bak s/qp7ctun6qt5n9d6/your_app_key/g index.html` (or, if you are building from source, change it [here](scripts/comp/dropbox-link.js#L7)) # Building 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: +To run Electron app without building installer, install electron package (`npm install electron-prebuilt -g`), build the app with `grunt` and start in this way: ```bash -$ cd electron -$ electron . --htmlpath=../tmp +$ grunt +$ electron electron --htmlpath=tmp ``` For debug build: From 5fc678091231abfd9f45543249e5066a85510092 Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 20:21:12 +0300 Subject: [PATCH 25/49] option to disable searching for group --- app/scripts/models/group-model.js | 10 +++++++++- app/scripts/views/grp-view.js | 9 ++++++++- app/styles/base/_forms.scss | 2 +- app/templates/grp.html | 6 ++++++ release-notes.md | 1 + 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/scripts/models/group-model.js b/app/scripts/models/group-model.js index 85554965..437e124e 100644 --- a/app/scripts/models/group-model.js +++ b/app/scripts/models/group-model.js @@ -16,7 +16,8 @@ var GroupModel = MenuItemModel.extend({ editable: true, top: false, drag: true, - drop: true + drop: true, + enableSearching: true }), initialize: function() { @@ -33,6 +34,7 @@ var GroupModel = MenuItemModel.extend({ items: new GroupCollection(), entries: new EntryCollection(), filterValue: group.uuid.id, + enableSearching: group.enableSearching, top: !parentGroup, drag: !!parentGroup }, { silent: true }); @@ -150,6 +152,12 @@ var GroupModel = MenuItemModel.extend({ this.set('expanded', expanded); }, + setEnableSearching: function(enabled) { + this._groupModified(); + this.group.enableSearching = enabled; + this.set('enableSearching', enabled); + }, + moveToTrash: function() { this.file.setModified(); this.file.db.remove(this.group); diff --git a/app/scripts/views/grp-view.js b/app/scripts/views/grp-view.js index b33cc184..c8eafe31 100644 --- a/app/scripts/views/grp-view.js +++ b/app/scripts/views/grp-view.js @@ -12,7 +12,8 @@ var GrpView = Backbone.View.extend({ 'click .grp__icon': 'showIconsSelect', 'click .grp__buttons-trash': 'moveToTrash', 'click .grp__back-button': 'returnToApp', - 'blur #grp__field-title': 'titleBlur' + 'blur #grp__field-title': 'titleBlur', + 'change #grp__check-search': 'setEnableSearching' }, initialize: function() { @@ -26,6 +27,7 @@ var GrpView = Backbone.View.extend({ 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') })); if (!this.model.get('title')) { @@ -107,6 +109,11 @@ var GrpView = Backbone.View.extend({ Backbone.trigger('select-all'); }, + setEnableSearching: function(e) { + var enabled = e.target.checked; + this.model.setEnableSearching(enabled); + }, + returnToApp: function() { Backbone.trigger('edit-group'); } diff --git a/app/styles/base/_forms.scss b/app/styles/base/_forms.scss index 45a40edf..929b3a6f 100644 --- a/app/styles/base/_forms.scss +++ b/app/styles/base/_forms.scss @@ -155,7 +155,7 @@ option { input[type=checkbox] { display: none; - & + label:hover:before { + &:not([disabled]) + label:hover:before { @include th { color: action-color(); } diff --git a/app/templates/grp.html b/app/templates/grp.html index 5e04f351..bcce46ba 100644 --- a/app/templates/grp.html +++ b/app/templates/grp.html @@ -9,6 +9,12 @@ />
+ <% if (!readonly) { %> +
+ /> + +
+ <% } %> <% if (customIcon) { %> diff --git a/release-notes.md b/release-notes.md index 28db7bf8..bbeeac11 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,7 @@ Release notes `*` default theme is now blue `+` #46: option to show colorful icons `+` #45: optional auto-lock on minimize +`+` option to disable searching for group `-` #55: custom scrollbar issues ##### v0.4.6 (2015-11-25) From 24a86abdbc3e8a2a801e6c881f615974fd7f48b8 Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 21:02:50 +0300 Subject: [PATCH 26/49] fix #62: allow empty password --- app/scripts/views/open-view.js | 4 +++- .../views/settings/settings-file-view.js | 17 ++++++++++++----- release-notes.md | 1 + 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index f6292872..5fa457a2 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -190,6 +190,8 @@ var OpenView = Backbone.View.extend({ if ($(e.target).is('.open__last-item-icon-del')) { this.model.removeFileInfo(id); this.$el.find('.open__last-item[data-id="' + id + '"]').remove(); + this.initialize(); + this.render(); return; } this.showOpenFileInfo(this.model.fileInfos.get(id)); @@ -197,7 +199,7 @@ var OpenView = Backbone.View.extend({ inputKeydown: function(e) { var code = e.keyCode || e.which; - if (code === Keys.DOM_VK_RETURN && this.passwordInput.length) { + if (code === Keys.DOM_VK_RETURN) { this.openDb(); } else if (code === Keys.DOM_VK_CAPS_LOCK) { this.$el.find('.open__pass-warning').removeClass('invisible'); diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index 9090f2bb..19c9f528 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -84,18 +84,25 @@ var SettingsAboutView = Backbone.View.extend({ validate: function() { if (!this.model.get('passwordLength')) { - Alerts.error({ + var that = this; + Alerts.yesno({ header: 'Empty password', - body: 'Please, enter the password. You will use it the next time you open this file.', - complete: (function() { this.$el.find('#settings__file-master-pass').focus(); }).bind(this) + body: 'Saving database with empty password makes it completely unprotected. Do you really want to do it?', + success: function() { + that.model.setPassword(kdbxweb.ProtectedValue.fromString('')); + that.saveToFile(true); + }, + cancel: function() { + that.$el.find('#settings__file-master-pass').focus(); + } }); return false; } return true; }, - saveToFile: function() { - if (!this.validate()) { + saveToFile: function(skipValidation) { + if (skipValidation !== true && !this.validate()) { return; } var that = this; diff --git a/release-notes.md b/release-notes.md index bbeeac11..d887a24e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -7,6 +7,7 @@ Release notes `+` #46: option to show colorful icons `+` #45: optional auto-lock on minimize `+` option to disable searching for group +`+` #62: saving files with empty password `-` #55: custom scrollbar issues ##### v0.4.6 (2015-11-25) From 169b22cb89976862e866bf944546f1d6ea64409d Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 22:18:35 +0300 Subject: [PATCH 27/49] file operations refactoring --- app/scripts/comp/dropbox-link.js | 4 ++ app/scripts/models/app-model.js | 38 ++++++++---- app/scripts/storage/storage-dropbox.js | 8 ++- app/scripts/views/app-view.js | 2 +- .../views/settings/settings-file-view.js | 61 ++++++++----------- app/scripts/views/settings/settings-view.js | 1 + app/templates/settings/settings-file.html | 10 +-- 7 files changed, 71 insertions(+), 53 deletions(-) diff --git a/app/scripts/comp/dropbox-link.js b/app/scripts/comp/dropbox-link.js index 97090dc4..6b514810 100644 --- a/app/scripts/comp/dropbox-link.js +++ b/app/scripts/comp/dropbox-link.js @@ -256,6 +256,10 @@ var DropboxLink = { this._callAndHandleError('readFile', [fileName, { arrayBuffer: true }], complete, errorAlertCallback); }, + stat: function(fileName, complete) { + this._callAndHandleError('stat', [fileName], complete); + }, + getFileList: function(complete) { this._callAndHandleError('readdir', [''], function(err, files, dirStat, filesStat) { if (files) { diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 8998eb64..7a1024e9 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -29,7 +29,6 @@ var AppModel = Backbone.Model.extend({ this.listenTo(Backbone, 'set-filter', this.setFilter); this.listenTo(Backbone, 'add-filter', this.addFilter); this.listenTo(Backbone, 'set-sort', this.setSort); - this.listenTo(Backbone, 'close-file', this.closeFile); this.listenTo(Backbone, 'empty-trash', this.emptyTrash); }, @@ -258,20 +257,37 @@ var AppModel = Backbone.Model.extend({ this.openFileFromCache(params, callback, fileInfo); } else { // try to load from storage and update cache - Storage[params.storage].load(params.path, function(err, data, rev) { - if (err) { - // failed to load from storage: fallback to cache if we can - if (fileInfo) { + var storage = Storage[params.storage]; + var storageLoad = function() { + storage.load(params.path, function(err, data, stat) { + if (err) { + // failed to load from storage: fallback to cache if we can + if (fileInfo) { + that.openFileFromCache(params, callback, fileInfo); + } else { + callback(err); + } + } else { + params.fileData = data; + params.rev = stat && stat.rev || null; + that.openFileWithData(params, callback, fileInfo, data, true); + } + }); + }; + var cacheRev = fileInfo && fileInfo.get('rev') || null; + if (cacheRev && storage.stat) { + storage.stat(params.path, function(err, stat) { + if (fileInfo && (err || stat && stat.rev === cacheRev)) { that.openFileFromCache(params, callback, fileInfo); + } else if (stat) { + storageLoad(); } else { callback(err); } - } else { - params.fileData = data; - params.rev = rev; - that.openFileWithData(params, callback, fileInfo, data, true); - } - }); + }); + } else { + storageLoad(); + } } }, diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js index 0afbf39e..a79d3d72 100644 --- a/app/scripts/storage/storage-dropbox.js +++ b/app/scripts/storage/storage-dropbox.js @@ -8,7 +8,13 @@ var StorageDropbox = { load: function(path, callback) { DropboxLink.openFile(path, function(err, data, stat) { - if (callback) { callback(err, data, stat ? stat.versionTag : null); } + if (callback) { callback(err, data, stat ? { rev: stat.versionTag } : null); } + }); + }, + + stat: function(path, callback) { + DropboxLink.stat(path, function(err, stat) { + if (callback) { callback(err, stat ? { rev: stat.versionTag } : null); } }); }, diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index f9187c8b..a39aba2b 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -166,7 +166,7 @@ var AppView = Backbone.View.extend({ this.views.details.hide(); this.views.grp.hide(); this.hideOpenFile(); - this.views.settings = new SettingsView(); + this.views.settings = new SettingsView({ model: this.model }); this.views.settings.setElement(this.$el.find('.app__body')).render(); if (!selectedMenuItem) { selectedMenuItem = this.model.menu.generalSection.get('items').first(); diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index 19c9f528..6e026bd1 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -15,6 +15,7 @@ var SettingsAboutView = Backbone.View.extend({ template: require('templates/settings/settings-file.html'), events: { + 'click .settings__file-button-save-default': 'saveDefault', 'click .settings__file-button-save-file': 'saveToFile', 'click .settings__file-button-export-xml': 'exportAsXml', 'click .settings__file-button-save-dropbox': 'saveToDropboxClick', @@ -32,6 +33,8 @@ var SettingsAboutView = Backbone.View.extend({ 'blur #settings__file-key-rounds': 'blurKeyRounds' }, + appModel: null, + initialize: function() { }, @@ -82,7 +85,7 @@ var SettingsAboutView = Backbone.View.extend({ } }, - validate: function() { + validatePassword: function(continueCallback) { if (!this.model.get('passwordLength')) { var that = this; Alerts.yesno({ @@ -90,7 +93,7 @@ var SettingsAboutView = Backbone.View.extend({ body: 'Saving database with empty password makes it completely unprotected. Do you really want to do it?', success: function() { that.model.setPassword(kdbxweb.ProtectedValue.fromString('')); - that.saveToFile(true); + continueCallback(); }, cancel: function() { that.$el.find('#settings__file-master-pass').focus(); @@ -101,53 +104,41 @@ var SettingsAboutView = Backbone.View.extend({ return true; }, + saveDefault: function() { + // TODO: save + //that.passwordChanged = false; + //that.model.saved(path, 'file'); + }, + saveToFile: function(skipValidation) { - if (skipValidation !== true && !this.validate()) { + if (skipValidation !== true && !this.validatePassword(this.saveToFile.bind(this, true))) { return; } var that = this; this.model.getData(function(data) { var fileName = that.model.get('name') + '.kdbx'; if (Launcher) { - if (that.model.get('path')) { - that.saveToFileWithPath(that.model.get('path'), data); - } else { - Launcher.getSaveFileName(fileName, function (path) { - if (path) { - that.saveToFileWithPath(path, data); - } - }); - } + Launcher.getSaveFileName(fileName, function (path) { + if (path) { + Storage.file.save(path, data, function(err) { + if (err) { + Alerts.error({ + header: 'Save error', + body: 'Error saving to file ' + path + ': \n' + err + }); + } + }); + } + }); } else { var blob = new Blob([data], {type: 'application/octet-stream'}); FileSaver.saveAs(blob, fileName); that.passwordChanged = false; - if (that.model.get('storage') !== 'dropbox') { - that.model.saved(); - } - } - }); - }, - - saveToFileWithPath: function(path, data) { - var that = this; - Storage.file.save(path, data, function(err) { - if (err) { - Alerts.error({ - header: 'Save error', - body: 'Error saving to file ' + path + ': \n' + err - }); - } else { - that.passwordChanged = false; - that.model.saved(path, 'file'); } }); }, exportAsXml: function() { - if (!this.validate()) { - return; - } this.model.getXml((function(xml) { var blob = new Blob([xml], {type: 'text/xml'}); FileSaver.saveAs(blob, this.model.get('name') + '.xml'); @@ -161,7 +152,7 @@ var SettingsAboutView = Backbone.View.extend({ }, saveToDropbox: function(overwrite) { - if (this.model.get('syncing') || !this.validate()) { + if (this.model.get('syncing') || !this.validatePassword()) { return; } var that = this; @@ -232,7 +223,7 @@ var SettingsAboutView = Backbone.View.extend({ }, closeFileNoCheck: function() { - Backbone.trigger('close-file', this.model); + this.appModel.closeFile(this.model); }, keyFileChange: function(e) { diff --git a/app/scripts/views/settings/settings-view.js b/app/scripts/views/settings/settings-view.js index 2c349e30..5ada9004 100644 --- a/app/scripts/views/settings/settings-view.js +++ b/app/scripts/views/settings/settings-view.js @@ -46,6 +46,7 @@ var SettingsView = Backbone.View.extend({ } var SettingsPageView = require('./settings-' + e.page + '-view'); this.views.page = new SettingsPageView({ el: this.pageEl, model: e.file }); + this.views.page.appModel = this.model; this.views.page.render(); this.file = e.file; this.page = e.page; diff --git a/app/templates/settings/settings-file.html b/app/templates/settings/settings-file.html index ad9db3c3..7745a1b6 100644 --- a/app/templates/settings/settings-file.html +++ b/app/templates/settings/settings-file.html @@ -7,18 +7,18 @@

This file is opened from Dropbox.

<% } %> <% } else { %> -

This database is loaded in memory. To enable auto-save and saving with shortcut <%= cmd %>S, - please, save it to <%= supportFiles ? 'file or ' : '' %>Dropbox.

+

This file is stored in browser.

<% if (!supportFiles) { %>

Want to work seamlessly with local files? Download a desktop app

<% } %> <% } %>
- + <% if (!storage || storage === 'file') { %><% } %> + + <% if (storage !== 'file') { %><% } %> -
From 60dd4431177d658a359c5674ac48d8a3f3b736b5 Mon Sep 17 00:00:00 2001 From: Antelle Date: Tue, 8 Dec 2015 22:22:22 +0300 Subject: [PATCH 28/49] release notes --- release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index d887a24e..dae3d795 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,7 +1,8 @@ Release notes ------------- ##### v0.5 (not released yet) -2-way sync +2-way merge sync +`*` all files are now opened with offline support `*` disallow opening same files twice `*` default theme is now blue `+` #46: option to show colorful icons From 5d823b959cee4e335b75ed727dee32b835d6ab61 Mon Sep 17 00:00:00 2001 From: Antelle Date: Wed, 9 Dec 2015 00:00:31 +0300 Subject: [PATCH 29/49] file save view --- app/scripts/models/app-model.js | 6 ++ app/scripts/models/file-model.js | 21 ----- app/scripts/views/open-view.js | 14 +-- .../views/settings/settings-file-view.js | 89 +++++++------------ 4 files changed, 48 insertions(+), 82 deletions(-) diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 7a1024e9..2f55dadd 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -355,6 +355,12 @@ var AppModel = Backbone.Model.extend({ Storage.cache.remove(id); this.fileInfos.remove(id); this.fileInfos.save(); + }, + + syncFile: function(file, options, callback) { + if (file.get('syncing')) { + return callback('Sync in progress'); + } } }); diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index a873f75f..0555ee90 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -216,26 +216,6 @@ var FileModel = Backbone.Model.extend({ } }, - autoSave: function(complete) { - var that = this; - that.set('syncing', true); - var storage = Storage[that.get('storage')]; - if (storage) { - that.getData(function(data) { - storage.save(that.get('path'), data, function (err) { - if (err) { - that.set('syncing', false); - } else { - that.saved(that.get('path'), that.get('storage')); - } - if (complete) { complete(err); } - }); - }); - } else { - throw 'Unknown storage; cannot auto save'; - } - }, - getData: function(cb) { this.db.cleanup({ historyRules: true, @@ -255,7 +235,6 @@ var FileModel = Backbone.Model.extend({ this.forEachEntry({}, function(entry) { entry.unsaved = false; }); - this.addToLastOpenFiles(); }, setPassword: function(password) { diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index 5fa457a2..7ad17978 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -368,13 +368,14 @@ var OpenView = Backbone.View.extend({ }, openDb: function() { - if (!this.busy) { - this.$el.toggleClass('open--opening', true); - this.inputEl.attr('disabled', 'disabled'); - this.busy = true; - this.params.password = this.passwordInput.value; - this.afterPaint(this.model.openFile.bind(this.model, this.params, this.openDbComplete.bind(this))); + if (this.busy || !this.params.name) { + return; } + this.$el.toggleClass('open--opening', true); + this.inputEl.attr('disabled', 'disabled'); + this.busy = true; + this.params.password = this.passwordInput.value; + this.afterPaint(this.model.openFile.bind(this.model, this.params, this.openDbComplete.bind(this))); }, openDbComplete: function(err) { @@ -382,6 +383,7 @@ var OpenView = Backbone.View.extend({ this.$el.toggleClass('open--opening', false); this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err); if (err) { + console.error('Error opening file', err); this.inputEl.focus(); this.inputEl[0].selectionStart = 0; this.inputEl[0].selectionEnd = this.inputEl.val().length; diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index 6e026bd1..bfbb75aa 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -18,7 +18,7 @@ var SettingsAboutView = Backbone.View.extend({ 'click .settings__file-button-save-default': 'saveDefault', 'click .settings__file-button-save-file': 'saveToFile', 'click .settings__file-button-export-xml': 'exportAsXml', - 'click .settings__file-button-save-dropbox': 'saveToDropboxClick', + 'click .settings__file-button-save-dropbox': 'saveToDropbox', 'click .settings__file-button-close': 'closeFile', 'change #settings__file-key-file': 'keyFileChange', 'mousedown #settings__file-file-select-link': 'triggerSelectFile', @@ -104,10 +104,33 @@ var SettingsAboutView = Backbone.View.extend({ return true; }, + save: function(arg) { + var that = this; + if (!arg) { + arg = {}; + } + arg.startedByUser = true; + if (!arg.skipValidation) { + var isValid = this.validatePassword(function() { + arg.skipValidation = true; + that.save(arg); + }); + if (!isValid) { + return; + } + } + this.appModel.syncFile(this.model, arg, function(err) { + if (err) { + console.error('Error saving file', err); + } else { + that.passwordChanged = false; + } + that.render(); + }); + }, + saveDefault: function() { - // TODO: save - //that.passwordChanged = false; - //that.model.saved(path, 'file'); + this.save(); }, saveToFile: function(skipValidation) { @@ -145,58 +168,14 @@ var SettingsAboutView = Backbone.View.extend({ }).bind(this)); }, - saveToDropboxClick: function() { - var nameChanged = this.model.get('path') !== this.model.get('name') + '.kdbx', - canOverwrite = !nameChanged; - this.saveToDropbox(canOverwrite); - }, - - saveToDropbox: function(overwrite) { - if (this.model.get('syncing') || !this.validatePassword()) { - return; - } + saveToDropbox: function() { var that = this; - this.model.getData(function(data) { - var fileName = that.model.get('name') + '.kdbx'; - that.model.set('syncing', true); - that.render(); - DropboxLink.authenticate(function(err) { - if (err) { - that.model.set('syncing', false); - that.render(); - return; - } - DropboxLink.saveFile(fileName, data, overwrite, function (err) { - if (err) { - that.model.set('syncing', false); - that.render(); - if (err.exists) { - Alerts.alert({ - header: 'Already exists', - body: 'File ' + fileName + ' already exists in your Dropbox.', - icon: 'question', - buttons: [{result: 'yes', title: 'Overwrite it'}, {result: '', title: 'I\'ll choose another name'}], - esc: '', - click: '', - enter: 'yes', - success: that.saveToDropbox.bind(that, true), - cancel: function () { - that.$el.find('#settings__file-name').focus(); - } - }); - } else { - Alerts.error({ - header: 'Save error', - body: 'Error saving to Dropbox: \n' + err - }); - } - } else { - that.passwordChanged = false; - that.model.saved(fileName, 'dropbox'); - that.render(); - } - }); - }); + DropboxLink.authenticate(function(err) { + if (err) { + that.render(); + return; + } + this.save({ storage: 'dropbox' }); }); }, From 00e22f0026aaeaa2cdd803e9dc6889a38bb61956 Mon Sep 17 00:00:00 2001 From: Antelle Date: Thu, 10 Dec 2015 22:44:02 +0300 Subject: [PATCH 30/49] sync method --- app/scripts/comp/dropbox-link.js | 7 ++- app/scripts/models/app-model.js | 85 +++++++++++++++++++++++++- app/scripts/models/file-model.js | 23 +++++-- app/scripts/storage/storage-dropbox.js | 4 +- 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/app/scripts/comp/dropbox-link.js b/app/scripts/comp/dropbox-link.js index 6b514810..d26d8698 100644 --- a/app/scripts/comp/dropbox-link.js +++ b/app/scripts/comp/dropbox-link.js @@ -239,9 +239,10 @@ var DropboxLink = { Dropbox.AuthDriver.Popup.oauthReceiver(); }, - saveFile: function(fileName, data, overwrite, complete) { - if (overwrite) { - this._callAndHandleError('writeFile', [fileName, data], complete); + saveFile: function(fileName, data, rev, complete) { + if (rev) { + var opts = typeof rev === 'string' ? { lastVersionTag: rev } : undefined; + this._callAndHandleError('writeFile', [fileName, data, opts], complete); } else { this.getFileList((function(err, files) { if (err) { return complete(err); } diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 2f55dadd..bdbc0fb4 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -318,7 +318,7 @@ var AppModel = Backbone.Model.extend({ return callback('Duplicate file id'); } var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); - if (updateCacheOnSuccess) { + if (updateCacheOnSuccess && params.storage !== 'file') { Storage.cache.save(cacheId, params.fileData, function(err) { if (err && !params.storage) { return; @@ -361,6 +361,89 @@ var AppModel = Backbone.Model.extend({ if (file.get('syncing')) { return callback('Sync in progress'); } + // todo: save to cache + var fileInfo = this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path')); + if (!fileInfo) { + var dt = new Date(); + fileInfo = new FileInfoModel({ + id: IdGenerator.uuid(), + name: file.get('name'), + storage: file.get('storage'), + path: file.get('path'), + modified: file.get('modified'), + editState: null, + rev: null, + pullDate: dt, + openDate: dt + }); + } + var storage = Storage[options.storage || file.get('storage')]; + if (!storage) { + if (!file.get('modified')) { + return callback(); + } + file.getData(function(data, err) { + if (err) { return callback(err); } + Storage.cache.save(fileInfo.id, data, function(err) { + callback(err); + }); + }); + } else { + var maxLoadLoops = 3, loadLoops = 0; + var loadFromStorageAndMerge = function() { + if (++loadLoops === maxLoadLoops) { + return callback('Too many load attempts, please try again later'); + } + storage.load(file.get('path'), function(err, data, stat) { + if (err) { return callback(err); } + file.merge(data, function(err) { + if (err) { return callback(err); } + if (stat && stat.rev) { + fileInfo.set('rev', stat.rev); + } + if (file.get('modified')) { + saveToStorage(); + } else { + callback(); + } + }); + }); + }; + var saveToStorage = function() { + file.getData(function(data, err) { + if (err) { return callback(err); } + storage.save(file.get('path'), data, function(err) { + if (err && err.revConflict) { + loadFromStorageAndMerge(); + } else if (err) { + callback(err); + } else { + if (storage === Storage.file) { + Storage.cache.remove(fileInfo.id); + } + callback(); + } + }, fileInfo.get('rev')); + }); + }; + if (options.reload) { + loadFromStorageAndMerge(); + } else if (storage === Storage.file) { + if (file.get('modified')) { + saveToStorage(); + } else { + callback(); + } + } else { + storage.stat(file.get('path'), function (err, stat) { + if (stat.rev === fileInfo.get('rev')) { + saveToStorage(); + } else { + loadFromStorageAndMerge(); + } + }); + } + } } }); diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 0555ee90..5471c760 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -3,7 +3,6 @@ var Backbone = require('backbone'), GroupCollection = require('../collections/group-collection'), GroupModel = require('./group-model'), - Storage = require('../storage'), IconUrl = require('../util/icon-url'), kdbxweb = require('kdbxweb'), demoFileData = require('base64!../../resources/Demo.kdbx'); @@ -29,7 +28,6 @@ var FileModel = Backbone.Model.extend({ }, db: null, - data: null, entryMap: null, groupMap: null, @@ -157,6 +155,18 @@ var FileModel = Backbone.Model.extend({ this.trigger('reload', this); }, + merge: function(fileData, callback) { + kdbxweb.Kdbx.load(fileData, this.db.credentials, (function(remoteDb, err) { + if (err) { + console.error('Error opening file to merge', err.code, err.message, err); + } else { + this.db.merge(remoteDb); + this.reload(); + } + callback(err); + }).bind(this)); + }, + close: function() { this.set({ keyFileName: '', @@ -221,8 +231,13 @@ var FileModel = Backbone.Model.extend({ historyRules: true, customIcons: true }); - this.data = this.db.save(cb); - return this.data; + var that = this; + this.db.save(function(data, err) { + if (err) { + console.error('Error saving file', that.get('name'), err); + } + cb(data, err); + }); }, getXml: function(cb) { diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js index a79d3d72..5ad01695 100644 --- a/app/scripts/storage/storage-dropbox.js +++ b/app/scripts/storage/storage-dropbox.js @@ -18,8 +18,8 @@ var StorageDropbox = { }); }, - save: function(path, data, callback) { - DropboxLink.saveFile(path, data, true, callback || _.noop); + save: function(path, data, callback, rev) { + DropboxLink.saveFile(path, data, rev, callback || _.noop); } }; From a28067055d1d3062bd0e883796f203d0e3e5e4a1 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 11 Dec 2015 00:31:47 +0300 Subject: [PATCH 31/49] sync --- .../collections/file-info-collection.js | 4 + app/scripts/models/app-model.js | 82 ++++++++++++------- app/scripts/models/file-model.js | 46 +++++++++-- app/scripts/views/app-view.js | 4 - .../views/settings/settings-file-view.js | 50 ++++++----- 5 files changed, 124 insertions(+), 62 deletions(-) diff --git a/app/scripts/collections/file-info-collection.js b/app/scripts/collections/file-info-collection.js index 45e5b505..dd8f555e 100644 --- a/app/scripts/collections/file-info-collection.js +++ b/app/scripts/collections/file-info-collection.js @@ -31,6 +31,10 @@ var FileInfoCollection = Backbone.Collection.extend({ (fi.get('name') || '') === (name || '') && (fi.get('path') || '') === (path || ''); }); + }, + + getByName: function() { + return this.find(function(file) { return file.get('name') === name; }); } }); diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index bdbc0fb4..2346a518 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -234,7 +234,7 @@ var AppModel = Backbone.Model.extend({ var name; for (var i = 0; ; i++) { name = 'New' + (i || ''); - if (!this.files.getByName(name)) { + if (!this.files.getByName(name) && !this.fileInfos.getByName(name)) { break; } } @@ -317,6 +317,12 @@ var AppModel = Backbone.Model.extend({ if (that.files.get(file.id)) { return callback('Duplicate file id'); } + if (fileInfo && fileInfo.get('modified')) { + if (fileInfo.get('editState')) { + file.setLocalEditState(fileInfo.get('editState')); + } + file.setModified(); + } var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); if (updateCacheOnSuccess && params.storage !== 'file') { Storage.cache.save(cacheId, params.fileData, function(err) { @@ -341,7 +347,7 @@ var AppModel = Backbone.Model.extend({ storage: file.get('storage'), path: file.get('path'), modified: file.get('modified'), - editState: null, + editState: file.getLocalEditState(), rev: rev, pullDate: dt, openDate: dt @@ -361,7 +367,10 @@ var AppModel = Backbone.Model.extend({ if (file.get('syncing')) { return callback('Sync in progress'); } - // todo: save to cache + var complete = function(err) { + // TODO: save file info + callback(err); + }; var fileInfo = this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path')); if (!fileInfo) { var dt = new Date(); @@ -378,66 +387,77 @@ var AppModel = Backbone.Model.extend({ }); } var storage = Storage[options.storage || file.get('storage')]; + var path = options.path || file.get('path'); if (!storage) { if (!file.get('modified')) { - return callback(); + return complete(); } file.getData(function(data, err) { - if (err) { return callback(err); } + if (err) { return complete(err); } Storage.cache.save(fileInfo.id, data, function(err) { - callback(err); + complete(err); }); }); } else { var maxLoadLoops = 3, loadLoops = 0; var loadFromStorageAndMerge = function() { if (++loadLoops === maxLoadLoops) { - return callback('Too many load attempts, please try again later'); + return complete('Too many load attempts, please try again later'); } - storage.load(file.get('path'), function(err, data, stat) { - if (err) { return callback(err); } - file.merge(data, function(err) { - if (err) { return callback(err); } + storage.load(path, function(err, data, stat) { + if (err) { return complete(err); } + file.mergeOrUpdate(data, function(err) { + if (err) { return complete(err); } if (stat && stat.rev) { fileInfo.set('rev', stat.rev); } if (file.get('modified')) { - saveToStorage(); + saveToCacheAndStorage(); } else { - callback(); + complete(); } }); }); }; - var saveToStorage = function() { + var saveToCacheAndStorage = function() { file.getData(function(data, err) { - if (err) { return callback(err); } - storage.save(file.get('path'), data, function(err) { - if (err && err.revConflict) { - loadFromStorageAndMerge(); - } else if (err) { - callback(err); - } else { - if (storage === Storage.file) { - Storage.cache.remove(fileInfo.id); - } - callback(); - } - }, fileInfo.get('rev')); + if (err) { return complete(err); } + if (storage === Storage.file) { + saveToStorage(data); + } else { + Storage.cache.save(fileInfo.id, data, function (err) { + if (err) { return complete(err); } + saveToStorage(data); + }); + } }); }; + var saveToStorage = function(data) { + storage.save(path, data, function(err) { + if (err && err.revConflict) { + loadFromStorageAndMerge(); + } else if (err) { + complete(err); + } else { + if (storage === Storage.file) { + Storage.cache.remove(fileInfo.id); + } + complete(); + } + }, fileInfo.get('rev')); + }; if (options.reload) { loadFromStorageAndMerge(); } else if (storage === Storage.file) { if (file.get('modified')) { - saveToStorage(); + saveToCacheAndStorage(); } else { - callback(); + complete(); } } else { - storage.stat(file.get('path'), function (err, stat) { + storage.stat(path, function (err, stat) { if (stat.rev === fileInfo.get('rev')) { - saveToStorage(); + saveToCacheAndStorage(); } else { loadFromStorageAndMerge(); } diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 5471c760..2e818080 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -24,7 +24,8 @@ var FileModel = Backbone.Model.extend({ oldKeyFileName: '', passwordChanged: false, keyFileChanged: false, - syncing: false + syncing: false, + syncError: null }, db: null, @@ -155,18 +156,39 @@ var FileModel = Backbone.Model.extend({ this.trigger('reload', this); }, - merge: function(fileData, callback) { + mergeOrUpdate: function(fileData, callback) { kdbxweb.Kdbx.load(fileData, this.db.credentials, (function(remoteDb, err) { if (err) { console.error('Error opening file to merge', err.code, err.message, err); } else { - this.db.merge(remoteDb); - this.reload(); + if (this.get('modified')) { + try { + this.db.merge(remoteDb); + } catch (e) { + console.error('File merge error', e); + return callback(e); + } + } else { + this.db = remoteDb; + this.reload(); + } } callback(err); }).bind(this)); }, + getLocalEditState: function() { + return this.db.getLocalEditState(); + }, + + setLocalEditState: function(editState) { + this.db.setLocalEditState(editState); + }, + + removeLocalEditState: function() { + this.db.removeLocalEditState(); + }, + close: function() { this.set({ keyFileName: '', @@ -244,8 +266,20 @@ var FileModel = Backbone.Model.extend({ this.db.saveXml(cb); }, - saved: function(path, storage) { - this.set({ path: path || '', storage: storage || null, modified: false, created: false, syncing: false }); + setSyncProgress: function() { + this.set({ syncing: true }); + }, + + setSyncComplete: function(path, storage, error) { + var modified = this.get('modified') && !!error; + this.set({ + created: false, + path: path || this.get('path'), + storage: storage || this.get('storage'), + modified: modified, + syncing: false, + syncError: error + }); this.setOpenFile({ passwordLength: this.get('passwordLength') }); this.forEachEntry({}, function(entry) { entry.unsaved = false; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index a39aba2b..acd25eb2 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -322,10 +322,6 @@ var AppView = Backbone.View.extend({ } }, - showVisualLock: function() { - // TODO: remove cases which lead to this - }, - saveAndLock: function(autoInit) { // TODO: move to file manager var pendingCallbacks = 0, diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index bfbb75aa..a0296bcf 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -137,28 +137,36 @@ var SettingsAboutView = Backbone.View.extend({ if (skipValidation !== true && !this.validatePassword(this.saveToFile.bind(this, true))) { return; } + var fileName = this.model.get('name') + '.kdbx'; var that = this; - this.model.getData(function(data) { - var fileName = that.model.get('name') + '.kdbx'; - if (Launcher) { - Launcher.getSaveFileName(fileName, function (path) { - if (path) { - Storage.file.save(path, data, function(err) { - if (err) { - Alerts.error({ - header: 'Save error', - body: 'Error saving to file ' + path + ': \n' + err - }); - } - }); - } - }); - } else { - var blob = new Blob([data], {type: 'application/octet-stream'}); - FileSaver.saveAs(blob, fileName); - that.passwordChanged = false; - } - }); + if (Launcher && !this.model.get('storage')) { + Launcher.getSaveFileName(fileName, function (path) { + if (path) { + that.save({storage: 'file', path: path}); + } + }); + } else { + this.model.getData(function (data) { + if (Launcher) { + Launcher.getSaveFileName(fileName, function (path) { + if (path) { + Storage.file.save(path, data, function (err) { + if (err) { + Alerts.error({ + header: 'Save error', + body: 'Error saving to file ' + path + ': \n' + err + }); + } + }); + } + }); + } else { + var blob = new Blob([data], {type: 'application/octet-stream'}); + FileSaver.saveAs(blob, fileName); + that.passwordChanged = false; + } + }); + } }, exportAsXml: function() { From 58aaec99387f1f34707f7ed188c65b2a9bc54d1a Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 11 Dec 2015 20:50:44 +0300 Subject: [PATCH 32/49] file dirty flag; further sync refactorint --- app/scripts/const/timeouts.js | 7 ++++ app/scripts/models/app-model.js | 31 ++++++++++++++---- app/scripts/models/file-model.js | 16 ++++++---- app/scripts/views/app-view.js | 55 ++++++++++---------------------- 4 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 app/scripts/const/timeouts.js diff --git a/app/scripts/const/timeouts.js b/app/scripts/const/timeouts.js new file mode 100644 index 00000000..7736cf70 --- /dev/null +++ b/app/scripts/const/timeouts.js @@ -0,0 +1,7 @@ +'use strict'; + +var Timeouts = { + AutoSync: 15000 +}; + +module.exports = Timeouts; diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 2346a518..1b0d07ee 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -321,7 +321,8 @@ var AppModel = Backbone.Model.extend({ if (fileInfo.get('editState')) { file.setLocalEditState(fileInfo.get('editState')); } - file.setModified(); + file.set('modified', true); + setTimeout(this.syncFile.bind(this, file), 0); } var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); if (updateCacheOnSuccess && params.storage !== 'file') { @@ -365,12 +366,11 @@ var AppModel = Backbone.Model.extend({ syncFile: function(file, options, callback) { if (file.get('syncing')) { - return callback('Sync in progress'); + return callback && callback('Sync in progress'); + } + if (!options) { + options = {}; } - var complete = function(err) { - // TODO: save file info - callback(err); - }; var fileInfo = this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path')); if (!fileInfo) { var dt = new Date(); @@ -388,6 +388,21 @@ var AppModel = Backbone.Model.extend({ } var storage = Storage[options.storage || file.get('storage')]; var path = options.path || file.get('path'); + var complete = function(err, savedToCache) { + if (!err) { savedToCache = true; } + file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache); + fileInfo.set({ + storage: storage, + path: path, + modified: file.get('modified'), + editState: file.getLocalEditState() + }); + if (!this.fileInfos.get(id)) { + this.fileInfos.unshift(fileInfo); + } + this.fileInfos.save(); + if (callback) { callback(err); } + }; if (!storage) { if (!file.get('modified')) { return complete(); @@ -411,6 +426,7 @@ var AppModel = Backbone.Model.extend({ if (stat && stat.rev) { fileInfo.set('rev', stat.rev); } + fileInfo.set('pullDate', new Date()); if (file.get('modified')) { saveToCacheAndStorage(); } else { @@ -422,11 +438,12 @@ var AppModel = Backbone.Model.extend({ var saveToCacheAndStorage = function() { file.getData(function(data, err) { if (err) { return complete(err); } - if (storage === Storage.file) { + if (!file.get('dirty') || storage === Storage.file) { saveToStorage(data); } else { Storage.cache.save(fileInfo.id, data, function (err) { if (err) { return complete(err); } + file.set('dirty', false); saveToStorage(data); }); } diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 2e818080..f25f56bb 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -16,6 +16,7 @@ var FileModel = Backbone.Model.extend({ path: '', storage: null, modified: false, + dirty: false, open: false, created: false, demo: false, @@ -164,6 +165,7 @@ var FileModel = Backbone.Model.extend({ if (this.get('modified')) { try { this.db.merge(remoteDb); + this.set('dirty', true); } catch (e) { console.error('File merge error', e); return callback(e); @@ -185,15 +187,12 @@ var FileModel = Backbone.Model.extend({ this.db.setLocalEditState(editState); }, - removeLocalEditState: function() { - this.db.removeLocalEditState(); - }, - close: function() { this.set({ keyFileName: '', passwordLength: 0, modified: false, + dirty: false, open: false, created: false, groups: null, @@ -244,7 +243,7 @@ var FileModel = Backbone.Model.extend({ setModified: function() { if (!this.get('demo')) { - this.set('modified', true); + this.set({ modified: true, dirty: true }); } }, @@ -270,13 +269,18 @@ var FileModel = Backbone.Model.extend({ this.set({ syncing: true }); }, - setSyncComplete: function(path, storage, error) { + setSyncComplete: function(path, storage, error, savedToCache) { + if (!error) { + this.db.removeLocalEditState(); + } var modified = this.get('modified') && !!error; + var dirty = this.get('dirty') && !savedToCache; this.set({ created: false, path: path || this.get('path'), storage: storage || this.get('storage'), modified: modified, + dirty: dirty, syncing: false, syncError: error }); diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index acd25eb2..f1d6c8bb 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -12,6 +12,7 @@ var Backbone = require('backbone'), SettingsView = require('../views/settings/settings-view'), Alerts = require('../comp/alerts'), Keys = require('../const/keys'), + Timeouts = require('../const/timeouts'), KeyHandler = require('../comp/key-handler'), IdleTracker = require('../comp/idle-tracker'), Launcher = require('../comp/launcher'), @@ -73,6 +74,8 @@ var AppView = Backbone.View.extend({ KeyHandler.onKey(Keys.DOM_VK_ESCAPE, this.escPressed, this); KeyHandler.onKey(Keys.DOM_VK_BACK_SPACE, this.backspacePressed, this); + + setInterval(this.syncAllByTimer.bind(this), Timeouts.AutoSync); }, render: function () { @@ -291,20 +294,18 @@ var AppView = Backbone.View.extend({ if (this.model.settings.get('autoSave')) { this.saveAndLock(autoInit); } else { - if (autoInit) { - this.showVisualLock('Auto-save is disabled. Please, enable it, to allow auto-locking'); - return; - } + var message = autoInit ? 'The app cannot be locked because auto save is disabled.' + : 'You have unsaved changes that will be lost. Continue?'; Alerts.alert({ icon: 'lock', header: 'Lock', - body: 'You have unsaved changes that will be lost. Continue?', + body: message, buttons: [ { result: 'save', title: 'Save changes' }, { result: 'discard', title: 'Discard changes', error: true }, { result: '', title: 'Cancel' } ], - checkbox: 'Auto save changes each time I lock the app', + checkbox: 'Save changes automatically', success: function(result, autoSaveChecked) { if (result === 'save') { if (autoSaveChecked) { @@ -323,27 +324,14 @@ var AppView = Backbone.View.extend({ }, saveAndLock: function(autoInit) { - // TODO: move to file manager var pendingCallbacks = 0, errorFiles = [], that = this; - if (this.model.files.some(function(file) { return file.get('modified') && !file.get('path'); })) { - this.showVisualLock('You have unsaved files, locking is not possible.'); - return; - } this.model.files.forEach(function(file) { - if (!file.get('modified')) { + if (!file.get('dirty')) { return; } - if (file.get('path')) { - try { - file.autoSave(fileSaved.bind(this, file)); - pendingCallbacks++; - } catch (e) { - console.error('Failed to auto-save file', file.get('path'), e); - errorFiles.push(file); - } - } + this.model.syncFile(file, null, fileSaved.bind(this, file)); }, this); if (!pendingCallbacks) { this.closeAllFilesAndShowFirst(); @@ -354,9 +342,7 @@ var AppView = Backbone.View.extend({ } if (--pendingCallbacks === 0) { if (errorFiles.length) { - if (autoInit) { - that.showVisualLock('Failed to save files: ' + errorFiles.join(', ')); - } else if (!Alerts.alertDisplayed) { + if (!Alerts.alertDisplayed) { Alerts.error({ header: 'Save Error', body: 'Failed to auto-save file' + (errorFiles.length > 1 ? 's: ' : '') + ' ' + errorFiles.join(', ') @@ -381,21 +367,14 @@ var AppView = Backbone.View.extend({ }, saveAll: function() { - var fileId; this.model.files.forEach(function(file) { - if (file.get('path')) { - try { - file.autoSave(); - } catch (e) { - console.error('Failed to auto-save file', file.get('path'), e); - fileId = file.cid; - } - } else if (!fileId) { - fileId = file.cid; - } - }); - if (fileId) { - this.showFileSettings({fileId: fileId}); + this.model.syncFile(file); + }, this); + }, + + syncAllByTimer: function() { + if (this.model.settings.get('autoSave')) { + this.saveAll(); } }, From bb8c65a081d39b5bd7b14f017850e5a846f0d217 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 11 Dec 2015 21:09:55 +0300 Subject: [PATCH 33/49] up electron --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 68b31d84..26f4f4da 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -16,7 +16,7 @@ module.exports = function(grunt) { var webpack = require('webpack'); var pkg = require('./package.json'); var dt = new Date().toISOString().replace(/T.*/, ''); - var electronVersion = '0.35.1'; + var electronVersion = '0.36.0'; function replaceFont(css) { css.walkAtRules('font-face', function (rule) { From c399dcdbf5a76e6a5a19a0e980e82077875eb671 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 11 Dec 2015 21:49:33 +0300 Subject: [PATCH 34/49] fix #63: entry table display user field --- app/templates/list-item-table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/list-item-table.html b/app/templates/list-item-table.html index 24821f32..60e4520e 100644 --- a/app/templates/list-item-table.html +++ b/app/templates/list-item-table.html @@ -2,7 +2,7 @@ <% if (customIcon) { %><% } else { %><% } %> <%- title || '(no title)' %> - <%- description %> + <%- user %> <%- url %> <%- tags %> <%- notes %> From 56b7feea9fb8dfa90a00530d76dbe1f2f36d3ac1 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 11 Dec 2015 22:06:15 +0300 Subject: [PATCH 35/49] update readme --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 86466e78..609d4222 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,6 @@ Twitter: [kee_web](https://twitter.com/kee_web) 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. -# Known Issues - -These major issues are in progress, or will be fixed in next releases, before v1.0: - -- dropbox sync is one-way: changes are not loaded from dropbox, only saved -- files are considered saved only when they are exported - # Self-hosting Everything you need to host this app on your server is any static file server. The app is a single HTML file + cache manifest (optionally; for offline access). From 5bd6cf88b488fd61c75b8b1d9f02d9927e2acae8 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 11 Dec 2015 23:51:16 +0300 Subject: [PATCH 36/49] sync bugfixes --- .idea/encodings.xml | 6 ++ app/scripts/comp/dropbox-link.js | 5 +- app/scripts/models/app-model.js | 56 +++++++++++++------ app/scripts/models/file-info-model.js | 2 +- app/scripts/models/file-model.js | 3 +- app/scripts/storage/storage-dropbox.js | 8 ++- app/scripts/views/app-view.js | 2 +- .../views/settings/settings-file-view.js | 2 +- bower.json | 2 +- keeweb.iml | 1 - 10 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 .idea/encodings.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..97626ba4 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/scripts/comp/dropbox-link.js b/app/scripts/comp/dropbox-link.js index d26d8698..82ec252d 100644 --- a/app/scripts/comp/dropbox-link.js +++ b/app/scripts/comp/dropbox-link.js @@ -121,6 +121,7 @@ DropboxChooser.prototype.readFile = function(url) { }; var DropboxLink = { + ERROR_CONFLICT: Dropbox.ApiError.CONFLICT, _getClient: function(complete) { if (this._dropboxClient && this._dropboxClient.isAuthenticated()) { complete(null, this._dropboxClient); @@ -199,6 +200,8 @@ var DropboxLink = { body: 'Something went wrong during Dropbox sync. Please, try again later. Error code: ' + err.status }); break; + case Dropbox.ApiError.CONFLICT: + break; default: alertCallback({ header: 'Dropbox Sync Error', @@ -241,7 +244,7 @@ var DropboxLink = { saveFile: function(fileName, data, rev, complete) { if (rev) { - var opts = typeof rev === 'string' ? { lastVersionTag: rev } : undefined; + var opts = typeof rev === 'string' ? { lastVersionTag: rev, noOverwrite: true, noAutoRename: true } : undefined; this._callAndHandleError('writeFile', [fileName, data, opts], complete); } else { this.getFileList((function(err, files) { diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 1b0d07ee..c900e8b4 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -322,7 +322,10 @@ var AppModel = Backbone.Model.extend({ file.setLocalEditState(fileInfo.get('editState')); } file.set('modified', true); - setTimeout(this.syncFile.bind(this, file), 0); + setTimeout(that.syncFile.bind(that, file), 0); + } + if (fileInfo) { + file.set('syncDate', fileInfo.get('syncDate')); } var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); if (updateCacheOnSuccess && params.storage !== 'file') { @@ -350,7 +353,7 @@ var AppModel = Backbone.Model.extend({ modified: file.get('modified'), editState: file.getLocalEditState(), rev: rev, - pullDate: dt, + syncDate: file.get('syncDate') || dt, openDate: dt }); this.fileInfos.remove(id); @@ -365,6 +368,10 @@ var AppModel = Backbone.Model.extend({ }, syncFile: function(file, options, callback) { + var that = this; + if (file.get('demo')) { + return callback && callback(); + } if (file.get('syncing')) { return callback && callback('Sync in progress'); } @@ -382,12 +389,13 @@ var AppModel = Backbone.Model.extend({ modified: file.get('modified'), editState: null, rev: null, - pullDate: dt, + syncDate: dt, openDate: dt }); } - var storage = Storage[options.storage || file.get('storage')]; + var storage = options.storage || file.get('storage'); var path = options.path || file.get('path'); + file.setSyncProgress(); var complete = function(err, savedToCache) { if (!err) { savedToCache = true; } file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache); @@ -395,12 +403,13 @@ var AppModel = Backbone.Model.extend({ storage: storage, path: path, modified: file.get('modified'), - editState: file.getLocalEditState() + editState: file.getLocalEditState(), + syncDate: file.get('syncDate') }); - if (!this.fileInfos.get(id)) { - this.fileInfos.unshift(fileInfo); + if (!that.fileInfos.get(fileInfo.id)) { + that.fileInfos.unshift(fileInfo); } - this.fileInfos.save(); + that.fileInfos.save(); if (callback) { callback(err); } }; if (!storage) { @@ -419,16 +428,22 @@ var AppModel = Backbone.Model.extend({ if (++loadLoops === maxLoadLoops) { return complete('Too many load attempts, please try again later'); } - storage.load(path, function(err, data, stat) { + Storage[storage].load(path, function(err, data, stat) { if (err) { return complete(err); } file.mergeOrUpdate(data, function(err) { if (err) { return complete(err); } if (stat && stat.rev) { fileInfo.set('rev', stat.rev); } - fileInfo.set('pullDate', new Date()); + file.set('syncDate', new Date()); if (file.get('modified')) { saveToCacheAndStorage(); + } else if (file.get('dirty') && storage !== 'file') { + Storage.cache.save(fileInfo.id, data, function (err) { + if (err) { return complete(err); } + file.set('dirty', false); + complete(); + }); } else { complete(); } @@ -438,7 +453,7 @@ var AppModel = Backbone.Model.extend({ var saveToCacheAndStorage = function() { file.getData(function(data, err) { if (err) { return complete(err); } - if (!file.get('dirty') || storage === Storage.file) { + if (!file.get('dirty') || storage === 'file') { saveToStorage(data); } else { Storage.cache.save(fileInfo.id, data, function (err) { @@ -450,31 +465,40 @@ var AppModel = Backbone.Model.extend({ }); }; var saveToStorage = function(data) { - storage.save(path, data, function(err) { + Storage[storage].save(path, data, function(err) { if (err && err.revConflict) { loadFromStorageAndMerge(); } else if (err) { complete(err); } else { - if (storage === Storage.file) { + if (storage === 'file') { Storage.cache.remove(fileInfo.id); } + file.set('syncDate', new Date()); complete(); } }, fileInfo.get('rev')); }; if (options.reload) { loadFromStorageAndMerge(); - } else if (storage === Storage.file) { + } else if (storage === 'file') { if (file.get('modified')) { saveToCacheAndStorage(); } else { complete(); } } else { - storage.stat(path, function (err, stat) { + Storage[storage].stat(path, function (err, stat) { + if (err) { + // TODO: save to cache if storage save failed + return complete(err); + } if (stat.rev === fileInfo.get('rev')) { - saveToCacheAndStorage(); + if (file.get('modified')) { + saveToCacheAndStorage(); + } else { + complete(); + } } else { loadFromStorageAndMerge(); } diff --git a/app/scripts/models/file-info-model.js b/app/scripts/models/file-info-model.js index 4b75d5d6..9ef05483 100644 --- a/app/scripts/models/file-info-model.js +++ b/app/scripts/models/file-info-model.js @@ -11,7 +11,7 @@ var FileInfoModel = Backbone.Model.extend({ modified: false, editState: null, rev: null, - pullDate: null, + syncDate: null, openDate: null }, diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index f25f56bb..01c94b55 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -26,7 +26,8 @@ var FileModel = Backbone.Model.extend({ passwordChanged: false, keyFileChanged: false, syncing: false, - syncError: null + syncError: null, + syncDate: null }, db: null, diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js index 5ad01695..2009c81b 100644 --- a/app/scripts/storage/storage-dropbox.js +++ b/app/scripts/storage/storage-dropbox.js @@ -19,7 +19,13 @@ var StorageDropbox = { }, save: function(path, data, callback, rev) { - DropboxLink.saveFile(path, data, rev, callback || _.noop); + DropboxLink.saveFile(path, data, rev, function(err) { + if (!callback) { return; } + if (err && err.status === DropboxLink.ERROR_CONFLICT) { + err = { revConflict: true }; + } + callback(err); + }); } }; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index f1d6c8bb..b32eccdc 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -323,7 +323,7 @@ var AppView = Backbone.View.extend({ } }, - saveAndLock: function(autoInit) { + saveAndLock: function(/*autoInit*/) { var pendingCallbacks = 0, errorFiles = [], that = this; diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index a0296bcf..3bb212eb 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -183,7 +183,7 @@ var SettingsAboutView = Backbone.View.extend({ that.render(); return; } - this.save({ storage: 'dropbox' }); + that.save({ storage: 'dropbox' }); }); }, diff --git a/bower.json b/bower.json index 67fee6b0..c9956bbd 100644 --- a/bower.json +++ b/bower.json @@ -26,7 +26,7 @@ "backbone": "~1.2.3", "baron": "~1.0.1", "bourbon": "~4.2.5", - "dropbox": "antelle/dropbox-js#0.10.5", + "dropbox": "antelle/dropbox-js#0.10.6", "font-awesome": "~4.4.0", "install": "~1.0.4", "kdbxweb": "~0.3.1", diff --git a/keeweb.iml b/keeweb.iml index 785b6491..0892b3c4 100644 --- a/keeweb.iml +++ b/keeweb.iml @@ -10,6 +10,5 @@ - \ No newline at end of file From f59cdeb10cd1121fd527b5d8c7bfdebb508fda33 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 12 Dec 2015 00:25:20 +0300 Subject: [PATCH 37/49] sync errors --- app/scripts/models/app-model.js | 19 +++++++++++++++---- app/styles/areas/_footer.scss | 1 + app/styles/base/_colors.scss | 1 + app/templates/footer.html | 6 ++++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index c900e8b4..031e29eb 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -490,10 +490,21 @@ var AppModel = Backbone.Model.extend({ } else { Storage[storage].stat(path, function (err, stat) { if (err) { - // TODO: save to cache if storage save failed - return complete(err); - } - if (stat.rev === fileInfo.get('rev')) { + if (file.get('dirty')) { + file.getData(function (data) { + if (data) { + Storage.cache.save(fileInfo.id, data, function (e) { + if (!e) { + file.set('dirty', false); + } + complete(err); + }); + } + }); + } else { + complete(err); + } + } else if (stat.rev === fileInfo.get('rev')) { if (file.get('modified')) { saveToCacheAndStorage(); } else { diff --git a/app/styles/areas/_footer.scss b/app/styles/areas/_footer.scss index 3521075d..7cef9ed7 100644 --- a/app/styles/areas/_footer.scss +++ b/app/styles/areas/_footer.scss @@ -32,6 +32,7 @@ right: 1em; top: 1em; @include th { color: action-color(); } + &--error { @include th { color: error-color(); } } } } diff --git a/app/styles/base/_colors.scss b/app/styles/base/_colors.scss index 6f31bd77..de9bd23c 100644 --- a/app/styles/base/_colors.scss +++ b/app/styles/base/_colors.scss @@ -59,3 +59,4 @@ $all-colors: ( } .muted-color { @include th { color: muted-color(); }; } .action-color { @include th { color: action-color(); }; } +.error-color { @include th { color: error-color(); }; } diff --git a/app/templates/footer.html b/app/templates/footer.html index 90841037..062a4294 100644 --- a/app/templates/footer.html +++ b/app/templates/footer.html @@ -2,8 +2,10 @@ <% files.forEach(function(file) { %> <% }); %> From 3dab9af06628b7108fbafdce03cd0e4dee4c3449 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 12 Dec 2015 11:53:50 +0300 Subject: [PATCH 38/49] logging sync --- .jshintrc | 2 - app/scripts/collections/file-collection.js | 4 ++ app/scripts/comp/dropbox-link.js | 47 +++++++++----- app/scripts/comp/settings-store.js | 9 ++- app/scripts/comp/transport.js | 13 ++-- app/scripts/comp/updater.js | 27 ++++---- app/scripts/const/timeouts.js | 2 +- app/scripts/models/app-model.js | 62 ++++++++++++++++++- app/scripts/models/file-model.js | 17 ++--- app/scripts/storage/storage-cache.js | 28 ++++++--- app/scripts/storage/storage-dropbox.js | 20 ++++-- app/scripts/storage/storage-file-cache.js | 22 +++++-- app/scripts/storage/storage-file.js | 15 ++++- app/scripts/util/logger.js | 42 +++++++++++++ app/scripts/views/app-view.js | 2 +- app/scripts/views/icon-select-view.js | 7 ++- app/scripts/views/open-view.js | 7 ++- .../views/settings/settings-file-view.js | 7 ++- app/templates/footer.html | 2 +- app/templates/settings/settings-file.html | 8 ++- 20 files changed, 263 insertions(+), 80 deletions(-) create mode 100644 app/scripts/util/logger.js diff --git a/.jshintrc b/.jshintrc index 9f6a4cc3..8e739bf2 100644 --- a/.jshintrc +++ b/.jshintrc @@ -88,8 +88,6 @@ "globals" : { "require": true, "module": true, - "console": true, - "performance": true, "$": true, "_": true } diff --git a/app/scripts/collections/file-collection.js b/app/scripts/collections/file-collection.js index db60fa57..aa9300a7 100644 --- a/app/scripts/collections/file-collection.js +++ b/app/scripts/collections/file-collection.js @@ -14,6 +14,10 @@ var FileCollection = Backbone.Collection.extend({ return this.some(function(file) { return file.get('modified'); }); }, + hasDirtyFiles: function() { + return this.some(function(file) { return file.get('dirty'); }); + }, + getByName: function(name) { return this.find(function(file) { return file.get('name') === name; }); }, diff --git a/app/scripts/comp/dropbox-link.js b/app/scripts/comp/dropbox-link.js index 82ec252d..bc24d82e 100644 --- a/app/scripts/comp/dropbox-link.js +++ b/app/scripts/comp/dropbox-link.js @@ -3,8 +3,11 @@ var Dropbox = require('dropbox'), Alerts = require('./alerts'), Launcher = require('./launcher'), + Logger = require('../util/logger'), Links = require('../const/links'); +var logger = new Logger('dropbox'); + var DropboxKeys = { AppFolder: 'qp7ctun6qt5n9d6', AppFolderKeyParts: ['qp7ctun6', 'qt5n9d6'] // to allow replace key by sed, compare in this way @@ -152,22 +155,29 @@ var DropboxLink = { _handleUiError: function(err, alertCallback, callback) { if (!alertCallback) { - alertCallback = Alerts.error.bind(Alerts); + if (!Alerts.alertDisplayed) { + alertCallback = Alerts.error.bind(Alerts); + } } - console.error('Dropbox error', err); + logger.error('Dropbox error', err); switch (err.status) { case Dropbox.ApiError.INVALID_TOKEN: - Alerts.yesno({ - icon: 'dropbox', - header: 'Dropbox Login', - body: 'To continue, you have to sign in to Dropbox.', - buttons: [{result: 'yes', title: 'Sign In'}, {result: '', title: 'Cancel'}], - success: (function() { - this.authenticate(function(err) { callback(!err); }); - }).bind(this), - cancel: function() { callback(false); } - }); - return; + if (!Alerts.alertDisplayed) { + Alerts.yesno({ + icon: 'dropbox', + header: 'Dropbox Login', + body: 'To continue, you have to sign in to Dropbox.', + buttons: [{result: 'yes', title: 'Sign In'}, {result: '', title: 'Cancel'}], + success: (function () { + this.authenticate(function (err) { callback(!err); }); + }).bind(this), + cancel: function () { + callback(false); + } + }); + return; + } + break; case Dropbox.ApiError.NOT_FOUND: alertCallback({ header: 'Dropbox Sync Error', @@ -218,7 +228,10 @@ var DropboxLink = { if (err) { return callback(err); } + var ts = logger.ts(); + logger.debug('Call', callName); client[callName].apply(client, args.concat(function(err) { + logger.debug('Result', callName, logger.ts(ts), arguments); if (err) { that._handleUiError(err, errorAlertCallback, function(repeat) { if (repeat) { @@ -242,10 +255,10 @@ var DropboxLink = { Dropbox.AuthDriver.Popup.oauthReceiver(); }, - saveFile: function(fileName, data, rev, complete) { + saveFile: function(fileName, data, rev, complete, alertCallback) { if (rev) { var opts = typeof rev === 'string' ? { lastVersionTag: rev, noOverwrite: true, noAutoRename: true } : undefined; - this._callAndHandleError('writeFile', [fileName, data, opts], complete); + this._callAndHandleError('writeFile', [fileName, data, opts], complete, alertCallback); } else { this.getFileList((function(err, files) { if (err) { return complete(err); } @@ -260,8 +273,8 @@ var DropboxLink = { this._callAndHandleError('readFile', [fileName, { arrayBuffer: true }], complete, errorAlertCallback); }, - stat: function(fileName, complete) { - this._callAndHandleError('stat', [fileName], complete); + stat: function(fileName, complete, errorAlertCallback) { + this._callAndHandleError('stat', [fileName], complete, errorAlertCallback); }, getFileList: function(complete) { diff --git a/app/scripts/comp/settings-store.js b/app/scripts/comp/settings-store.js index 37313d0f..7bcf6fcf 100644 --- a/app/scripts/comp/settings-store.js +++ b/app/scripts/comp/settings-store.js @@ -1,7 +1,10 @@ 'use strict'; var Launcher = require('./launcher'), - StringUtil = require('../util/string-util'); + StringUtil = require('../util/string-util'), + Logger = require('../util/logger'); + +var logger = new Logger('settings'); var SettingsStore = { fileName: function(key) { @@ -20,7 +23,7 @@ var SettingsStore = { return data ? JSON.parse(data) : undefined; } } catch (e) { - console.error('Error loading ' + key, e); + logger.error('Error loading ' + key, e); } return null; }, @@ -33,7 +36,7 @@ var SettingsStore = { localStorage[StringUtil.camelCase(key)] = JSON.stringify(data); } } catch (e) { - console.error('Error saving ' + key, e); + logger.error('Error saving ' + key, e); } } }; diff --git a/app/scripts/comp/transport.js b/app/scripts/comp/transport.js index b8f147da..a54c1a44 100644 --- a/app/scripts/comp/transport.js +++ b/app/scripts/comp/transport.js @@ -1,6 +1,9 @@ 'use strict'; -var Launcher = require('./launcher'); +var Launcher = require('./launcher'), + Logger = require('../util/logger'); + +var logger = new Logger('transport'); var Transport = { httpGet: function(config) { @@ -11,7 +14,7 @@ var Transport = { if (fs.existsSync(tmpFile)) { try { if (config.cache && fs.statSync(tmpFile).size > 0) { - console.log('File already downloaded ' + config.url); + logger.info('File already downloaded ' + config.url); return config.success(tmpFile); } else { fs.unlinkSync(tmpFile); @@ -22,11 +25,11 @@ var Transport = { } } var proto = config.url.split(':')[0]; - console.log('GET ' + config.url); + 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) { - console.log('Response from ' + config.url + ': ' + res.statusCode); + logger.info('Response from ' + config.url + ': ' + res.statusCode); if (res.statusCode === 200) { if (config.file) { var file = fs.createWriteStream(tmpFile); @@ -57,7 +60,7 @@ var Transport = { config.error('HTTP status ' + res.statusCode); } }).on('error', function(e) { - console.error('Cannot GET ' + config.url, e); + logger.error('Cannot GET ' + config.url, e); if (tmpFile) { fs.unlink(tmpFile); } diff --git a/app/scripts/comp/updater.js b/app/scripts/comp/updater.js index 41b53763..54c18201 100644 --- a/app/scripts/comp/updater.js +++ b/app/scripts/comp/updater.js @@ -6,7 +6,10 @@ var Backbone = require('backbone'), Launcher = require('../comp/launcher'), AppSettingsModel = require('../models/app-settings-model'), UpdateModel = require('../models/update-model'), - Transport = require('../comp/transport'); + Transport = require('../comp/transport'), + Logger = require('../util/logger'); + +var logger = new Logger('updater'); var Updater = { UpdateInterval: 1000*60*60*24, @@ -57,7 +60,7 @@ var Updater = { timeDiff = Math.min(Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout), this.UpdateInterval); } this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff); - console.log('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's'); + logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's'); return timeDiff === this.MinUpdateTimeout; }, @@ -74,20 +77,20 @@ var Updater = { // 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); + logger.error('Prevented update check; last check was performed at ' + this.updateCheckDate); that.scheduleNextCheck(); return; } this.updateCheckDate = new Date(); } - console.log('Checking for update...'); + logger.info('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')); + logger.info('Update check: ' + (match ? match[0] : 'unknown')); if (!match) { var errMsg = 'No version info found'; UpdateModel.instance.set({ status: 'error', lastCheckDate: dt, lastCheckError: errMsg }); @@ -108,7 +111,7 @@ var Updater = { that.scheduleNextCheck(); if (prevLastVersion === UpdateModel.instance.get('lastVersion') && UpdateModel.instance.get('updateStatus') === 'ready') { - console.log('Waiting for the user to apply downloaded update'); + logger.info('Waiting for the user to apply downloaded update'); return; } if (!startedByUser && that.getAutoUpdateType() === 'install') { @@ -118,7 +121,7 @@ var Updater = { } }, error: function(e) { - console.error('Update check error', e); + logger.error('Update check error', e); UpdateModel.instance.set({ status: 'error', lastCheckDate: new Date(), @@ -140,22 +143,22 @@ var Updater = { update: function(startedByUser, successCallback) { var ver = UpdateModel.instance.get('lastVersion'); if (!Launcher || ver === RuntimeInfo.version) { - console.log('You are using the latest version'); + logger.info('You are using the latest version'); return; } UpdateModel.instance.set({ updateStatus: 'downloading', updateError: null }); var that = this; - console.log('Downloading update', ver); + logger.info('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); + logger.info('Extracting update file', that.UpdateCheckFiles, filePath); that.extractAppUpdate(filePath, function(err) { if (err) { - console.error('Error extracting update', err); + logger.error('Error extracting update', err); UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error extracting update' }); } else { UpdateModel.instance.set({ updateStatus: 'ready', updateError: null }); @@ -169,7 +172,7 @@ var Updater = { }); }, error: function(e) { - console.error('Error downloading update', e); + logger.error('Error downloading update', e); UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error downloading update' }); } }); diff --git a/app/scripts/const/timeouts.js b/app/scripts/const/timeouts.js index 7736cf70..48f5f592 100644 --- a/app/scripts/const/timeouts.js +++ b/app/scripts/const/timeouts.js @@ -1,7 +1,7 @@ 'use strict'; var Timeouts = { - AutoSync: 15000 + AutoSync: 15 * 1000 * 60 }; module.exports = Timeouts; diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 031e29eb..b0635ef2 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -11,7 +11,8 @@ var Backbone = require('backbone'), FileModel = require('./file-model'), FileInfoModel = require('./file-info-model'), Storage = require('../storage'), - IdGenerator = require('../util/id-generator'); + IdGenerator = require('../util/id-generator'), + Logger = require('../util/logger'); var AppModel = Backbone.Model.extend({ defaults: {}, @@ -30,6 +31,8 @@ var AppModel = Backbone.Model.extend({ this.listenTo(Backbone, 'add-filter', this.addFilter); this.listenTo(Backbone, 'set-sort', this.setSort); this.listenTo(Backbone, 'empty-trash', this.emptyTrash); + + this.appLogger = new Logger('app'); }, addFile: function(file) { @@ -244,30 +247,40 @@ var AppModel = Backbone.Model.extend({ }, openFile: function(params, callback) { + var logger = new Logger('open', params.name); + logger.info('File open request'); var that = this; var fileInfo = params.id ? this.fileInfos.get(params.id) : this.fileInfos.getMatch(params.storage, params.name, params.path); if (fileInfo && fileInfo.get('modified')) { // modified offline, cannot overwrite: load from cache + logger.info('Open file from cache because it is modified'); this.openFileFromCache(params, callback, fileInfo); } else if (params.fileData) { // has user content: load it + logger.info('Open file from supplied content'); this.openFileWithData(params, callback, fileInfo, params.fileData, true); } else if (fileInfo && fileInfo.get('rev') === params.rev) { // already latest in cache: use it + logger.info('Open file from cache because it is latest'); this.openFileFromCache(params, callback, fileInfo); } else { // try to load from storage and update cache + logger.info('Open file from storage', params.storage); var storage = Storage[params.storage]; var storageLoad = function() { + logger.info('Load from storage'); storage.load(params.path, function(err, data, stat) { if (err) { // failed to load from storage: fallback to cache if we can if (fileInfo) { + logger.info('Open file from cache because of storage load error', err); that.openFileFromCache(params, callback, fileInfo); } else { + logger.info('Storage load error', err); callback(err); } } else { + logger.info('Open file from content loaded from storage'); params.fileData = data; params.rev = stat && stat.rev || null; that.openFileWithData(params, callback, fileInfo, data, true); @@ -276,12 +289,16 @@ var AppModel = Backbone.Model.extend({ }; var cacheRev = fileInfo && fileInfo.get('rev') || null; if (cacheRev && storage.stat) { + logger.info('Stat file'); storage.stat(params.path, function(err, stat) { if (fileInfo && (err || stat && stat.rev === cacheRev)) { + logger.info('Open file from cache because it is latest or stat error', err); that.openFileFromCache(params, callback, fileInfo); } else if (stat) { + logger.info('Open file from storage'); storageLoad(); } else { + logger.info('Stat error', err); callback(err); } }); @@ -294,6 +311,7 @@ var AppModel = Backbone.Model.extend({ openFileFromCache: function(params, callback, fileInfo) { var that = this; Storage.cache.load(fileInfo.id, function(err, data) { + new Logger('open', params.name).info('Loaded file from cache', params.name, err); if (err) { callback(err); } else { @@ -303,6 +321,7 @@ var AppModel = Backbone.Model.extend({ }, openFileWithData: function(params, callback, fileInfo, data, updateCacheOnSuccess) { + var logger = new Logger('open', params.name); var file = new FileModel({ name: params.name, storage: params.storage, @@ -319,8 +338,10 @@ var AppModel = Backbone.Model.extend({ } if (fileInfo && fileInfo.get('modified')) { if (fileInfo.get('editState')) { + logger.info('Loaded local edit state'); file.setLocalEditState(fileInfo.get('editState')); } + logger.info('Mark file as modified and schedule sync'); file.set('modified', true); setTimeout(that.syncFile.bind(that, file), 0); } @@ -329,6 +350,7 @@ var AppModel = Backbone.Model.extend({ } var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); if (updateCacheOnSuccess && params.storage !== 'file') { + logger.info('Save loaded file to cache'); Storage.cache.save(cacheId, params.fileData, function(err) { if (err && !params.storage) { return; @@ -344,6 +366,7 @@ var AppModel = Backbone.Model.extend({ }, addToLastOpenFiles: function(file, id, rev) { + this.appLogger.debug('Add last open file', id, file.get('name'), file.get('storage'), file.get('path')); var dt = new Date(); var fileInfo = new FileInfoModel({ id: id, @@ -378,8 +401,13 @@ var AppModel = Backbone.Model.extend({ if (!options) { options = {}; } + var logger = new Logger('sync', file.get('name')); + var storage = options.storage || file.get('storage'); + var path = options.path || file.get('path'); + logger.info('Sync started', storage, path, options); var fileInfo = this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path')); if (!fileInfo) { + logger.info('Create new file info'); var dt = new Date(); fileInfo = new FileInfoModel({ id: IdGenerator.uuid(), @@ -393,11 +421,10 @@ var AppModel = Backbone.Model.extend({ openDate: dt }); } - var storage = options.storage || file.get('storage'); - var path = options.path || file.get('path'); file.setSyncProgress(); var complete = function(err, savedToCache) { if (!err) { savedToCache = true; } + logger.info('Sync finished', savedToCache ? 'saved to cache' : '', err); file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache); fileInfo.set({ storage: storage, @@ -414,103 +441,132 @@ var AppModel = Backbone.Model.extend({ }; if (!storage) { if (!file.get('modified')) { + logger.info('Local, not modified'); return complete(); } + logger.info('Local, save to cache'); file.getData(function(data, err) { if (err) { return complete(err); } Storage.cache.save(fileInfo.id, data, function(err) { + logger.info('Saved to cache', err); complete(err); }); }); } else { var maxLoadLoops = 3, loadLoops = 0; var loadFromStorageAndMerge = function() { + logger.info('Load from storage, attempt ' + loadLoops); if (++loadLoops === maxLoadLoops) { return complete('Too many load attempts, please try again later'); } Storage[storage].load(path, function(err, data, stat) { + logger.info('Load from storage', stat, err); if (err) { return complete(err); } file.mergeOrUpdate(data, function(err) { + logger.info('Merge complete', err); if (err) { return complete(err); } if (stat && stat.rev) { + logger.info('Update rev'); fileInfo.set('rev', stat.rev); } file.set('syncDate', new Date()); if (file.get('modified')) { + logger.info('Updated sync date, saving modified file to cache and storage'); saveToCacheAndStorage(); } else if (file.get('dirty') && storage !== 'file') { + logger.info('Saving not modified dirty file to cache'); Storage.cache.save(fileInfo.id, data, function (err) { if (err) { return complete(err); } file.set('dirty', false); + logger.info('Complete, remove dirty flag'); complete(); }); } else { + logger.info('Complete, no changes'); complete(); } }); }); }; var saveToCacheAndStorage = function() { + 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'); saveToStorage(data); } else { + logger.info('Saving to cache'); Storage.cache.save(fileInfo.id, data, function (err) { if (err) { return complete(err); } file.set('dirty', false); + logger.info('Saved to cache, saving to storage'); saveToStorage(data); }); } }); }; var saveToStorage = function(data) { + logger.info('Save data to storage'); Storage[storage].save(path, data, function(err) { if (err && err.revConflict) { + logger.info('Save rev conflict, reloading from storage'); loadFromStorageAndMerge(); } else if (err) { + logger.info('Error saving data to storage'); complete(err); } else { if (storage === 'file') { Storage.cache.remove(fileInfo.id); } file.set('syncDate', new Date()); + logger.info('Save to storage complete, update sync date'); complete(); } }, fileInfo.get('rev')); }; if (options.reload) { + logger.info('Saved to cache'); loadFromStorageAndMerge(); } else if (storage === 'file') { if (file.get('modified')) { + 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 (file.get('dirty')) { + logger.info('Stat error, dirty, save to cache', err); file.getData(function (data) { if (data) { Storage.cache.save(fileInfo.id, data, function (e) { if (!e) { file.set('dirty', false); } + logger.info('Saved to cache, exit with error', err); complete(err); }); } }); } else { + logger.info('Stat error, not dirty', err); complete(err); } } else if (stat.rev === fileInfo.get('rev')) { if (file.get('modified')) { + logger.info('Stat found same version, modified, saving to cache and storage'); saveToCacheAndStorage(); } else { + logger.info('Stat found same version, not modified'); complete(); } } else { + logger.info('Found new version, loading from storage'); loadFromStorageAndMerge(); } }); diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 01c94b55..09c557bc 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -4,9 +4,12 @@ var Backbone = require('backbone'), GroupCollection = require('../collections/group-collection'), GroupModel = require('./group-model'), IconUrl = require('../util/icon-url'), + Logger = require('../util/logger'), kdbxweb = require('kdbxweb'), demoFileData = require('base64!../../resources/Demo.kdbx'); +var logger = new Logger('file'); + var FileModel = Backbone.Model.extend({ defaults: { id: '', @@ -56,10 +59,10 @@ var FileModel = Backbone.Model.extend({ password = new kdbxweb.ProtectedValue(value.buffer.slice(0, byteLength), salt.buffer.slice(0, byteLength)); try { var credentials = new kdbxweb.Credentials(password, keyFileData); - var start = performance.now(); + var ts = logger.ts(); kdbxweb.Kdbx.load(fileData, credentials, (function(db, err) { if (err) { - console.error('Error opening file', err.code, err.message, err); + logger.error('Error opening file', err.code, err.message, err); callback(err); } else { this.db = db; @@ -68,13 +71,13 @@ var FileModel = Backbone.Model.extend({ if (keyFileData) { kdbxweb.ByteUtils.zeroBuffer(keyFileData); } - console.log('Opened file ' + this.get('name') + ': ' + Math.round(performance.now() - start) + 'ms, ' + + logger.info('Opened file ' + this.get('name') + ': ' + logger.ts(ts) + ', ' + db.header.keyEncryptionRounds + ' rounds, ' + Math.round(fileData.byteLength / 1024) + ' kB'); callback(); } }).bind(this)); } catch (e) { - console.error('Error opening file', e, e.code, e.message, e); + logger.error('Error opening file', e, e.code, e.message, e); callback(e); } }, @@ -161,14 +164,14 @@ var FileModel = Backbone.Model.extend({ mergeOrUpdate: function(fileData, callback) { kdbxweb.Kdbx.load(fileData, this.db.credentials, (function(remoteDb, err) { if (err) { - console.error('Error opening file to merge', err.code, err.message, err); + logger.error('Error opening file to merge', err.code, err.message, err); } else { if (this.get('modified')) { try { this.db.merge(remoteDb); this.set('dirty', true); } catch (e) { - console.error('File merge error', e); + logger.error('File merge error', e); return callback(e); } } else { @@ -256,7 +259,7 @@ var FileModel = Backbone.Model.extend({ var that = this; this.db.save(function(data, err) { if (err) { - console.error('Error saving file', that.get('name'), err); + logger.error('Error saving file', that.get('name'), err); } cb(data, err); }); diff --git a/app/scripts/storage/storage-cache.js b/app/scripts/storage/storage-cache.js index 8405decf..99c00e76 100644 --- a/app/scripts/storage/storage-cache.js +++ b/app/scripts/storage/storage-cache.js @@ -1,5 +1,8 @@ 'use strict'; +var Logger = require('../util/logger'); + +var logger = new Logger('storage-cache'); var idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; var StorageCache = { @@ -17,7 +20,7 @@ var StorageCache = { try { var req = idb.open('FilesCache'); req.onerror = function (e) { - console.error('Error opening indexed db', e); + logger.error('Error opening indexed db', e); that.errorOpening = e; if (callback) { callback(e); } }; @@ -30,69 +33,78 @@ var StorageCache = { db.createObjectStore('files'); }; } catch (e) { - console.error('Error opening indexed db', e); + logger.error('Error opening indexed db', e); if (callback) { callback(e); } } }, save: function(id, data, callback) { + logger.debug('Save', id); this.init((function(err) { if (err) { return callback && callback(err); } try { + var ts = logger.ts(); var req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id); req.onsuccess = function () { + logger.debug('Saved', id, logger.ts(ts)); if (callback) { callback(); } }; req.onerror = function () { - console.error('Error saving to cache', id, req.error); + logger.error('Error saving to cache', id, req.error); if (callback) { callback(req.error); } }; } catch (e) { - console.error('Error saving to cache', id, e); + logger.error('Error saving to cache', id, e); if (callback) { callback(e); } } }).bind(this)); }, load: function(id, callback) { + logger.debug('Load', id); this.init((function(err) { if (err) { return callback && callback(err, null); } try { + var ts = logger.ts(); var req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id); req.onsuccess = function () { + logger.debug('Loaded', id, logger.ts(ts)); if (callback) { callback(null, req.result); } }; req.onerror = function () { - console.error('Error loading from cache', id, req.error); + logger.error('Error loading from cache', id, req.error); if (callback) { callback(req.error); } }; } catch (e) { - console.error('Error loading from cache', id, e); + logger.error('Error loading from cache', id, e); if (callback) { callback(e, null); } } }).bind(this)); }, remove: function(id, callback) { + logger.debug('Remove', id); this.init((function(err) { if (err) { return callback && callback(err); } try { + var ts = logger.ts(); var req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id); req.onsuccess = function () { + logger.debug('Removed', id, logger.ts(ts)); if (callback) { callback(); } }; req.onerror = function () { - console.error('Error removing from cache', id, req.error); + logger.error('Error removing from cache', id, req.error); if (callback) { callback(req.error); } }; } catch(e) { - console.error('Error removing from cache', id, e); + logger.error('Error removing from cache', id, e); if (callback) { callback(e); } } }).bind(this)); diff --git a/app/scripts/storage/storage-dropbox.js b/app/scripts/storage/storage-dropbox.js index 2009c81b..2a3230a2 100644 --- a/app/scripts/storage/storage-dropbox.js +++ b/app/scripts/storage/storage-dropbox.js @@ -1,31 +1,43 @@ 'use strict'; -var DropboxLink = require('../comp/dropbox-link'); +var DropboxLink = require('../comp/dropbox-link'), + Logger = require('../util/logger'); + +var logger = new Logger('storage-dropbox'); var StorageDropbox = { name: 'dropbox', enabled: true, load: function(path, callback) { + logger.debug('Load', path); + var ts = logger.ts(); DropboxLink.openFile(path, function(err, data, stat) { + logger.debug('Loaded', path, stat ? stat.versionTag : null, logger.ts(ts)); if (callback) { callback(err, data, stat ? { rev: stat.versionTag } : null); } - }); + }, _.noop); }, stat: function(path, callback) { + logger.debug('Stat', path); + var ts = logger.ts(); DropboxLink.stat(path, function(err, stat) { + logger.debug('Stated', path, stat ? stat.versionTag : null, logger.ts(ts)); if (callback) { callback(err, stat ? { rev: stat.versionTag } : null); } - }); + }, _.noop); }, save: function(path, data, callback, rev) { + logger.debug('Save', path, rev); + var ts = logger.ts(); DropboxLink.saveFile(path, data, rev, function(err) { + logger.debug('Saved', path, logger.ts(ts)); if (!callback) { return; } if (err && err.status === DropboxLink.ERROR_CONFLICT) { err = { revConflict: true }; } callback(err); - }); + }, _.noop); } }; diff --git a/app/scripts/storage/storage-file-cache.js b/app/scripts/storage/storage-file-cache.js index dcc57d73..185f0a2d 100644 --- a/app/scripts/storage/storage-file-cache.js +++ b/app/scripts/storage/storage-file-cache.js @@ -1,6 +1,9 @@ 'use strict'; -var Launcher = require('../comp/launcher'); +var Launcher = require('../comp/launcher'), + Logger = require('../util/logger'); + +var logger = new Logger('storage-file-cache'); var StorageFileCache = { name: 'cache', @@ -24,54 +27,63 @@ var StorageFileCache = { } this.path = path; } catch (e) { - console.error('Error opening local offline storage', e); + logger.error('Error opening local offline storage', e); if (callback) { callback(e); } } }, save: function(id, data, callback) { + logger.debug('Save', id); this.init((function(err) { if (err) { return callback && callback(err); } + var ts = logger.ts(); try { Launcher.writeFile(this.getPath(id), data); + logger.debug('Saved', id, logger.ts(ts)); if (callback) { callback(); } } catch (e) { - console.error('Error saving to cache', id, e); + logger.error('Error saving to cache', id, e); if (callback) { callback(e); } } }).bind(this)); }, load: function(id, callback) { + logger.debug('Load', id); this.init((function(err) { if (err) { return callback && callback(null, err); } + var ts = logger.ts(); try { var data = Launcher.readFile(this.getPath(id)); + logger.debug('Loaded', id, logger.ts(ts)); if (callback) { callback(null, data.buffer); } } catch (e) { - console.error('Error loading from cache', id, e); + logger.error('Error loading from cache', id, e); if (callback) { callback(e, null); } } }).bind(this)); }, remove: function(id, callback) { + logger.debug('Remove', id); this.init((function(err) { if (err) { return callback && callback(err); } + var ts = logger.ts(); try { var path = this.getPath(id); if (Launcher.fileExists(path)) { Launcher.deleteFile(path); } + logger.debug('Removed', id, logger.ts(ts)); if (callback) { callback(); } } catch(e) { - console.error('Error removing from cache', id, e); + logger.error('Error removing from cache', id, e); if (callback) { callback(e); } } }).bind(this)); diff --git a/app/scripts/storage/storage-file.js b/app/scripts/storage/storage-file.js index b0256f2d..ec4aa1e2 100644 --- a/app/scripts/storage/storage-file.js +++ b/app/scripts/storage/storage-file.js @@ -1,27 +1,36 @@ 'use strict'; -var Launcher = require('../comp/launcher'); +var Launcher = require('../comp/launcher'), + Logger = require('../util/logger'); + +var logger = new Logger('storage-file'); var StorageFile = { name: 'file', enabled: !!Launcher, load: function(path, callback) { + logger.debug('Load', path); + var ts = logger.ts(); try { var data = Launcher.readFile(path); + logger.debug('Loaded', path, logger.ts(ts)); if (callback) { callback(null, data.buffer); } } catch (e) { - console.error('Error reading local file', path, e); + logger.error('Error reading local file', path, e); if (callback) { callback(e, null); } } }, save: function(path, data, callback) { + logger.debug('Save', path); + var ts = logger.ts(); try { Launcher.writeFile(path, data); + logger.debug('Saved', path, logger.ts(ts)); if (callback) { callback(); } } catch (e) { - console.error('Error writing local file', path, e); + logger.error('Error writing local file', path, e); if (callback) { callback(e); } } } diff --git a/app/scripts/util/logger.js b/app/scripts/util/logger.js new file mode 100644 index 00000000..28c0650f --- /dev/null +++ b/app/scripts/util/logger.js @@ -0,0 +1,42 @@ +'use strict'; + +/* globals console */ +/* globals performance */ + +var Logger = function(name, id) { + this.prefix = (name ? name + (id ? ':' + id : '') : 'default'); +}; + +Logger.prototype.ts = function(ts) { + if (ts) { + return Math.round(performance.now() - ts) + 'ms'; + } else { + return performance.now(); + } +}; + +Logger.prototype.getPrefix = function() { + return new Date().toISOString() + ' [' + this.prefix + '] '; +}; + +Logger.prototype.debug = function() { + arguments[0] = this.getPrefix() + arguments[0]; + console.debug.apply(console, arguments); +}; + +Logger.prototype.info = function() { + arguments[0] = this.getPrefix() + arguments[0]; + console.log.apply(console, arguments); +}; + +Logger.prototype.warn = function() { + arguments[0] = this.getPrefix() + arguments[0]; + console.warn.apply(console, arguments); +}; + +Logger.prototype.error = function() { + arguments[0] = this.getPrefix() + arguments[0]; + console.error.apply(console, arguments); +}; + +module.exports = Logger; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index b32eccdc..0926637b 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -218,7 +218,7 @@ var AppView = Backbone.View.extend({ }, beforeUnload: function(e) { - if (this.model.files.hasUnsavedFiles()) { + if (this.model.files.hasDirtyFiles()) { if (Launcher && !Launcher.exitRequested) { if (!this.exitAlertShown) { var that = this; diff --git a/app/scripts/views/icon-select-view.js b/app/scripts/views/icon-select-view.js index a41e4e03..d18d085c 100644 --- a/app/scripts/views/icon-select-view.js +++ b/app/scripts/views/icon-select-view.js @@ -2,7 +2,10 @@ var Backbone = require('backbone'), IconMap = require('../const/icon-map'), - Launcher = require('../comp/launcher'); + Launcher = require('../comp/launcher'), + Logger = require('../util/logger'); + +var logger = new Logger('icon-select-view'); var IconSelectView = Backbone.View.extend({ template: require('templates/icon-select.html'), @@ -67,7 +70,7 @@ var IconSelectView = Backbone.View.extend({ that.downloadingFavicon = false; }; img.onerror = function (e) { - console.error('Favicon download error: ' + url, e); + logger.error('Favicon download error: ' + url, e); that.$el.find('.icon-select__icon-download>i').removeClass('fa-spinner fa-spin'); that.$el.find('.icon-select__icon-download').removeClass('icon-select__icon--custom-selected'); that.downloadingFavicon = false; diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js index 7ad17978..8dd1a1a4 100644 --- a/app/scripts/views/open-view.js +++ b/app/scripts/views/open-view.js @@ -4,7 +4,10 @@ var Backbone = require('backbone'), Keys = require('../const/keys'), Alerts = require('../comp/alerts'), SecureInput = require('../comp/secure-input'), - DropboxLink = require('../comp/dropbox-link'); + DropboxLink = require('../comp/dropbox-link'), + Logger = require('../util/logger'); + +var logger = new Logger('open-view'); var OpenView = Backbone.View.extend({ template: require('templates/open.html'), @@ -383,7 +386,7 @@ var OpenView = Backbone.View.extend({ this.$el.toggleClass('open--opening', false); this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err); if (err) { - console.error('Error opening file', err); + logger.error('Error opening file', err); this.inputEl.focus(); this.inputEl[0].selectionStart = 0; this.inputEl[0].selectionEnd = this.inputEl.val().length; diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index 3bb212eb..6151c5eb 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -8,6 +8,7 @@ var Backbone = require('backbone'), Storage = require('../../storage'), Links = require('../../const/links'), DropboxLink = require('../../comp/dropbox-link'), + Format = require('../../util/format'), kdbxweb = require('kdbxweb'), FileSaver = require('filesaver'); @@ -48,6 +49,8 @@ var SettingsAboutView = Backbone.View.extend({ path: this.model.get('path'), storage: this.model.get('storage'), syncing: this.model.get('syncing'), + syncError: this.model.get('syncError'), + syncDate: Format.dtStr(this.model.get('syncDate')), password: PasswordGenerator.present(this.model.get('passwordLength')), defaultUser: this.model.get('defaultUser'), recycleBinEnabled: this.model.get('recycleBinEnabled'), @@ -120,9 +123,7 @@ var SettingsAboutView = Backbone.View.extend({ } } this.appModel.syncFile(this.model, arg, function(err) { - if (err) { - console.error('Error saving file', err); - } else { + if (!err) { that.passwordChanged = false; } that.render(); diff --git a/app/templates/footer.html b/app/templates/footer.html index 062a4294..86222d64 100644 --- a/app/templates/footer.html +++ b/app/templates/footer.html @@ -3,7 +3,7 @@ diff --git a/app/templates/settings/settings-file.html b/app/templates/settings/settings-file.html index 7745a1b6..0f0370b3 100644 --- a/app/templates/settings/settings-file.html +++ b/app/templates/settings/settings-file.html @@ -16,12 +16,18 @@
<% if (!storage || storage === 'file') { %><% } %> + <%= syncing ? 'disabled' : '' %>>Sync with Dropbox <% if (storage !== 'file') { %><% } %>
+ <% if (storage) { %> +

Sync

+
Last sync: <%= syncDate || 'unknown' %> <%= syncing ? '(sync in progress...)' : '' %>
+ <% if (syncError) { %>
Sync error: <%- syncError %>
<% } %> + <% } %> +

Settings