From f94493a82c3c27d8d74efdc45ba599bcb9d715dd Mon Sep 17 00:00:00 2001 From: antelle Date: Sun, 14 Aug 2016 19:18:51 +0300 Subject: [PATCH] generator presets --- app/scripts/comp/generator-presets.js | 127 ++++++++++++++++ app/scripts/util/locale.js | 14 +- app/scripts/util/password-generator.js | 3 + app/scripts/views/generator-presets-view.js | 155 +++++++++++++++----- app/scripts/views/generator-view.js | 49 +++---- app/scripts/views/grp-view.js | 2 +- app/styles/areas/_generator-presets.scss | 25 +++- app/styles/areas/_generator.scss | 2 + app/styles/areas/_grp.scss | 11 +- app/templates/generator-presets.hbs | 59 +++++--- app/templates/grp.hbs | 12 +- release-notes.md | 1 + 12 files changed, 362 insertions(+), 98 deletions(-) create mode 100644 app/scripts/comp/generator-presets.js diff --git a/app/scripts/comp/generator-presets.js b/app/scripts/comp/generator-presets.js new file mode 100644 index 00000000..b6639ee0 --- /dev/null +++ b/app/scripts/comp/generator-presets.js @@ -0,0 +1,127 @@ +'use strict'; + +const AppSettingsModel = require('../models/app-settings-model'); +const Locale = require('../util/locale'); + +let GeneratorPresets = { + get defaultPreset() { + return { name: 'Default', title: Locale.genPresetDefault, + length: 16, upper: true, lower: true, digits: true }; + }, + + get builtIn() { + return [ + this.defaultPreset, + { name: 'Pronounceable', title: Locale.genPresetPronounceable, + length: 10, lower: true, upper: true }, + { name: 'Med', title: Locale.genPresetMed, + length: 16, upper: true, lower: true, digits: true, special: true, brackets: true, ambiguous: true }, + { name: 'Long', title: Locale.genPresetLong, + length: 32, upper: true, lower: true, digits: true }, + { name: 'Pin4', title: Locale.genPresetPin4, + length: 4, digits: true }, + { name: 'Mac', title: Locale.genPresetMac, + length: 17, upper: true, digits: true, special: true }, + { name: 'Hash128', title: Locale.genPresetHash128, + length: 32, lower: true, digits: true }, + { name: 'Hash256', title: Locale.genPresetHash256, + length: 64, lower: true, digits: true } + ]; + }, + + get all() { + let presets = this.builtIn; + presets.forEach(preset => { preset.builtIn = true; }); + let setting = AppSettingsModel.instance.get('generatorPresets'); + if (setting) { + if (setting.user) { + presets = presets.concat(setting.user.map(_.clone)); + } + let hasDefault = false; + presets.forEach(preset => { + if (setting.disabled && setting.disabled[preset.name]) { + preset.disabled = true; + } + if (setting.default === preset.name) { + hasDefault = true; + preset.default = true; + } + }); + if (!hasDefault) { + presets[0].default = true; + } + } + return presets; + }, + + get enabled() { + let allPresets = this.all.filter(preset => !preset.disabled); + if (!allPresets.length) { + allPresets.push(this.defaultPreset); + } + return allPresets; + }, + + getOrCreateSetting() { + let setting = AppSettingsModel.instance.get('generatorPresets'); + if (!setting) { + setting = { user: [] }; + } + return setting; + }, + + add(preset) { + let setting = this.getOrCreateSetting(); + if (preset.name && !setting.user.filter(p => p.name === preset.name).length) { + setting.user.push(preset); + this.save(setting); + } + }, + + remove(name) { + let setting = this.getOrCreateSetting(); + setting.user = setting.user.filter(p => p.name !== name); + this.save(setting); + }, + + setPreset(name, props) { + let setting = this.getOrCreateSetting(); + let preset = setting.user.filter(p => p.name === name)[0]; + if (preset) { + _.extend(preset, props); + this.save(setting); + } + }, + + setDisabled(name, disabled) { + let setting = this.getOrCreateSetting(); + if (disabled) { + if (!setting.disabled) { + setting.disabled = {}; + } + setting.disabled[name] = true; + } else { + if (setting.disabled) { + delete setting.disabled[name]; + } + } + this.save(setting); + }, + + setDefault(name) { + let setting = this.getOrCreateSetting(); + if (name) { + setting.default = name; + } else { + delete setting.default; + } + this.save(setting); + }, + + save: function(setting) { + AppSettingsModel.instance.unset('generatorPresets', { silent: true }); + AppSettingsModel.instance.set('generatorPresets', setting); + } +}; + +module.exports = GeneratorPresets; diff --git a/app/scripts/util/locale.js b/app/scripts/util/locale.js index 8a3c1c59..8ea328b2 100644 --- a/app/scripts/util/locale.js +++ b/app/scripts/util/locale.js @@ -89,11 +89,21 @@ var Locale = { tagBadNameBody: 'Tag name can not contain characters `,`, `;`, `:`. Please remove them.', genPsTitle: 'Generator Presets', - genPsEmpty: 'You have no presets yet', - genPsEmptyDesc: 'Presets allow you to generate passwords by your rules faster', genPsCreate: 'New preset', genPsDelete: 'Delete preset', genPsNew: 'preset', + genPsEnabled: 'Show in presets list', + genPsDefault: 'Selected by default', + genPsDefaultLength: 'Default length', + genPsUpper: 'Uppercase latin letters', + genPsLower: 'Lowercase latin letters', + genPsDigits: 'Digits', + genPsSpecial: 'Special symbols', + genPsBrackets: 'Brackets', + genPsHigh: 'High ASCII characters', + genPsAmbiguous: 'Ambiguous symbols', + genPsInclude: 'Additional symbols to include', + genPsExample: 'Example of generated password', keyChangeTitleRemote: 'Master Key Changed', keyChangeMessageRemote: 'Master key was changed for this database. Please enter a new key', diff --git a/app/scripts/util/password-generator.js b/app/scripts/util/password-generator.js index c4d49ccc..026bec17 100644 --- a/app/scripts/util/password-generator.js +++ b/app/scripts/util/password-generator.js @@ -31,6 +31,9 @@ var PasswordGenerator = { var ranges = Object.keys(this.charRanges) .filter(r => opts[r]) .map(function(r) { return this.charRanges[r]; }, this); + if (opts.include && opts.include.length) { + ranges.push(opts.include); + } if (!ranges.length) { return ''; } diff --git a/app/scripts/views/generator-presets-view.js b/app/scripts/views/generator-presets-view.js index 952a7b81..aca69b1d 100644 --- a/app/scripts/views/generator-presets-view.js +++ b/app/scripts/views/generator-presets-view.js @@ -1,7 +1,10 @@ 'use strict'; const Backbone = require('backbone'); +const Scrollable = require('../mixins/scrollable'); const Locale = require('../util/locale'); +const GeneratorPresets = require('../comp/generator-presets'); +const PasswordGenerator = require('../util/password-generator'); let GeneratorPresetsView = Backbone.View.extend({ template: require('templates/generator-presets.hbs'), @@ -11,87 +14,165 @@ let GeneratorPresetsView = Backbone.View.extend({ 'change .gen-ps__list': 'changePreset', 'click .gen-ps__btn-create': 'createPreset', 'click .gen-ps__btn-delete': 'deletePreset', - 'input #gen-ps__field-name': 'changeName' + 'input #gen-ps__field-title': 'changeTitle', + 'change #gen-ps__check-enabled': 'changeEnabled', + 'change #gen-ps__check-default': 'changeDefault', + 'input #gen-ps__field-length': 'changeLength', + 'change .gen-ps__check-range': 'changeRange', + 'input #gen-ps__field-include': 'changeInclude' }, selected: null, + reservedTitles: [Locale.genPresetDerived], + initialize: function() { this.appModel = this.model; }, render: function() { - let presets = this.appModel.settings.get('generatorPresets') || []; - if (!this.selected || presets.indexOf(this.selected) < 0) { - this.selected = presets[0]; + this.presets = GeneratorPresets.all; + if (!this.selected || !this.presets.some(p => p.name === this.selected)) { + this.selected = (this.presets.filter(p => p.default)[0] || this.presets[0]).name; } this.renderTemplate({ - empty: !presets.length, - presets: presets, - selected: this.selected + presets: this.presets, + selected: this.getPreset(this.selected), + ranges: this.getSelectedRanges() }, true); + this.createScroll({ + root: this.$el.find('.gen-ps')[0], + scroller: this.$el.find('.scroller')[0], + bar: this.$el.find('.scroller__bar')[0] + }); + this.renderExample(); return this; }, + renderExample: function() { + let selectedPreset = this.getPreset(this.selected); + let example = PasswordGenerator.generate(selectedPreset); + this.$el.find('.gen-ps__example').text(example); + this.pageResized(); + }, + + getSelectedRanges: function() { + let sel = this.getPreset(this.selected); + let rangeOverride = { + high: '¡¢£¤¥¦§©ª«¬®¯°±¹²´µ¶»¼÷¿ÀÖîü...' + }; + return ['Upper', 'Lower', 'Digits', 'Special', 'Brackets', 'High', 'Ambiguous'].map(name => { + let nameLower = name.toLowerCase(); + return { + name: nameLower, + title: Locale['genPs' + name], + enabled: sel[nameLower], + sample: rangeOverride[nameLower] || PasswordGenerator.charRanges[nameLower] + }; + }); + }, + + getPreset: function(name) { + return this.presets.filter(p => p.name === name)[0]; + }, + returnToApp: function() { Backbone.trigger('edit-generator-presets'); }, changePreset: function(e) { - let id = e.target.value; - let presets = this.appModel.settings.get('generatorPresets'); - this.selected = presets.filter(p => p.id === id)[0]; + this.selected = e.target.value; this.render(); }, createPreset: function() { - let presets = this.appModel.settings.get('generatorPresets') || []; let name; - let id; + let title; for (let i = 1; ; i++) { - let newName = Locale.genPsNew + ' ' + i; - if (!presets.filter(p => p.name === newName).length) { + let newName = 'Custom' + i; + let newTitle = Locale.genPsNew + ' ' + i; + if (!this.presets.filter(p => p.name === newName || p.title === newTitle).length) { name = newName; + title = newTitle; break; } } - for (let i = 1; ; i++) { - let newId = 'custom' + i; - if (!presets.filter(p => p.id === newId).length) { - id = newId; - break; - } - } - let preset = { id, name }; - presets.push(preset); - this.selected = preset; - this.appModel.settings.set('generatorPresets', presets); + let selected = this.getPreset(this.selected); + let preset = { + name, title, + length: selected.length, + upper: selected.upper, lower: selected.lower, digits: selected.digits, + special: selected.special, brackets: selected.brackets, ambiguous: selected.ambiguous, + include: selected.include + }; + GeneratorPresets.add(preset); + this.selected = name; this.render(); }, deletePreset: function() { - let presets = this.appModel.settings.get('generatorPresets'); - presets = presets.filter(p => p.id !== this.selected.id); - this.appModel.settings.set('generatorPresets', presets.length ? presets : null); + GeneratorPresets.remove(this.selected); this.render(); }, - changeName: function(e) { - let name = $.trim(e.target.value); - if (name && name !== this.selected.name) { - let presets = this.appModel.settings.get('generatorPresets'); - let another = presets.filter(p => p.name.toLowerCase() === name.toLowerCase())[0]; - if (another) { + changeTitle: function(e) { + let title = $.trim(e.target.value); + if (title && title !== this.getPreset(this.selected).title) { + let duplicate = this.presets.some(p => p.title.toLowerCase() === title.toLowerCase()); + if (!duplicate) { + duplicate = this.reservedTitles.some(p => p.toLowerCase() === title.toLowerCase()); + } + if (duplicate) { $(e.target).addClass('input--error'); return; } else { $(e.target).removeClass('input--error'); } - this.selected.name = name; - this.appModel.settings.set('generatorPresets', presets); - this.$el.find('.gen-ps__list option[selected]').text(name); + GeneratorPresets.setPreset(this.selected, { title }); + this.$el.find('.gen-ps__list option[selected]').text(title); } + }, + + changeEnabled: function(e) { + let enabled = e.target.checked; + GeneratorPresets.setDisabled(this.selected, !enabled); + }, + + changeDefault: function(e) { + let isDefault = e.target.checked; + GeneratorPresets.setDefault(isDefault ? this.selected : null); + }, + + changeLength: function(e) { + let length = +e.target.value; + if (length > 0) { + GeneratorPresets.setPreset(this.selected, { length }); + $(e.target).removeClass('input--error'); + } else { + $(e.target).addClass('input--error'); + } + this.presets = GeneratorPresets.all; + this.renderExample(); + }, + + changeRange: function(e) { + let enabled = e.target.checked; + let range = e.target.dataset.range; + GeneratorPresets.setPreset(this.selected, { [range]: enabled }); + this.presets = GeneratorPresets.all; + this.renderExample(); + }, + + changeInclude: function(e) { + let include = e.target.value; + if (include !== this.getPreset(this.selected).include) { + GeneratorPresets.setPreset(this.selected, { include: include }); + } + this.presets = GeneratorPresets.all; + this.renderExample(); } }); +_.extend(GeneratorPresetsView.prototype, Scrollable); + module.exports = GeneratorPresetsView; diff --git a/app/scripts/views/generator-view.js b/app/scripts/views/generator-view.js index bb2c89bc..9d255504 100644 --- a/app/scripts/views/generator-view.js +++ b/app/scripts/views/generator-view.js @@ -1,9 +1,10 @@ 'use strict'; -var Backbone = require('backbone'), - PasswordGenerator = require('../util/password-generator'), - CopyPaste = require('../comp/copy-paste'), - Locale = require('../util/locale'); +const Backbone = require('backbone'); +const PasswordGenerator = require('../util/password-generator'); +const CopyPaste = require('../comp/copy-paste'); +const GeneratorPresets = require('../comp/generator-presets'); +const Locale = require('../util/locale'); var GeneratorView = Backbone.View.extend({ el: 'body', @@ -13,7 +14,6 @@ var GeneratorView = Backbone.View.extend({ events: { 'click': 'click', 'mousedown .gen__length-range': 'generate', - 'mousemove .gen__length-range': 'lengthChange', 'change .gen__length-range': 'lengthChange', 'change .gen__check input[type=checkbox]': 'checkChange', 'click .gen__btn-ok': 'btnOkClick', @@ -45,39 +45,30 @@ var GeneratorView = Backbone.View.extend({ }, createPresets: function() { - this.presets = [ - { name: 'Default', length: 16, upper: true, lower: true, digits: true }, - { name: 'Pronounceable', length: 10, lower: true, upper: true }, - { name: 'Med', length: 16, upper: true, lower: true, digits: true, special: true, brackets: true, ambiguous: true }, - { name: 'Long', length: 32, upper: true, lower: true, digits: true }, - { name: 'Pin4', length: 4, digits: true }, - { name: 'Mac', length: 17, upper: true, digits: true, special: true }, - { name: 'Hash128', length: 32, lower: true, digits: true }, - { name: 'Hash256', length: 64, lower: true, digits: true } - ]; + this.presets = GeneratorPresets.enabled; if (this.model.password && (!this.model.password.isProtected || this.model.password.byteLength)) { - var derivedPreset = { name: 'Derived' }; + var derivedPreset = { name: 'Derived', title: Locale.genPresetDerived }; _.extend(derivedPreset, PasswordGenerator.deriveOpts(this.model.password)); - for (var i = 0; i < this.valuesMap.length; i++) { - if (this.valuesMap[i] >= derivedPreset.length) { - derivedPreset.length = this.valuesMap[i]; - break; - } - } - if (derivedPreset.length > this.valuesMap[this.valuesMap.length - 1]) { - derivedPreset.length = this.valuesMap[this.valuesMap.length - 1]; - } - this.presets.splice(1, 0, derivedPreset); + this.presets.splice(0, 0, derivedPreset); this.preset = 'Derived'; } else { - this.preset = 'Default'; + let defaultPreset = this.presets.filter(p => p.default)[0] || this.presets[0]; + this.preset = defaultPreset.name; } this.presets.forEach(function(pr) { - pr.pseudoLength = this.valuesMap.indexOf(pr.length); - pr.title = Locale['genPreset' + pr.name]; + pr.pseudoLength = this.lengthToPseudoValue(pr.length); }, this); }, + lengthToPseudoValue: function(length) { + for (let ix = 0; ix < this.valuesMap.length; ix++) { + if (this.valuesMap[ix] >= length) { + return ix; + } + } + return this.valuesMap.length - 1; + }, + click: function(e) { e.stopPropagation(); }, diff --git a/app/scripts/views/grp-view.js b/app/scripts/views/grp-view.js index f484beaa..e93f99a5 100644 --- a/app/scripts/views/grp-view.js +++ b/app/scripts/views/grp-view.js @@ -43,7 +43,7 @@ var GrpView = Backbone.View.extend({ } } this.createScroll({ - root: this.$el.find('.details__body')[0], + root: this.$el.find('.grp')[0], scroller: this.$el.find('.scroller')[0], bar: this.$el.find('.scroller__bar')[0] }); diff --git a/app/styles/areas/_generator-presets.scss b/app/styles/areas/_generator-presets.scss index 5a8d3c1e..5839d044 100644 --- a/app/styles/areas/_generator-presets.scss +++ b/app/styles/areas/_generator-presets.scss @@ -4,11 +4,32 @@ @include align-items(stretch); @include flex-direction(column); @include justify-content(flex-start); + @include scrollbar-on-hover; width: 100%; user-select: none; + overflow: hidden; + position: relative; - &__empty-text { - padding-bottom: $large-padding; + >.scroller { + @include flex(1); + overflow-x: hidden; + } + + &__buttons { + margin-top: $base-padding-v; + } + + &__sample { + font-weight: normal; + @include th { color: muted-color(); } + } + + &__example { + @include user-select(text); + font-family: $monospace-font-family; + margin-top: 0; + white-space: pre-wrap; + word-break: break-all; } &__list, &__input { diff --git a/app/styles/areas/_generator.scss b/app/styles/areas/_generator.scss index 09ad0e5c..7bf0c118 100644 --- a/app/styles/areas/_generator.scss +++ b/app/styles/areas/_generator.scss @@ -30,10 +30,12 @@ @include user-select(text); font-family: $monospace-font-family; margin-top: 6px; + margin-bottom: 3px; height: 50px; text-align: center; white-space: pre-wrap; word-break: break-all; + overflow: hidden; &--long-pass { font-size: .75em; } diff --git a/app/styles/areas/_grp.scss b/app/styles/areas/_grp.scss index 5f722f56..6ce65ee7 100644 --- a/app/styles/areas/_grp.scss +++ b/app/styles/areas/_grp.scss @@ -7,15 +7,12 @@ @include scrollbar-on-hover; width: 100%; user-select: none; + overflow: hidden; + position: relative; >.scroller { @include flex(1); - @include display(flex); - @include align-items(stretch); - @include flex-direction(column); - @include justify-content(flex-start); overflow-x: hidden; - padding-top: 3px; } &__icon { @@ -30,6 +27,10 @@ } } + &__icon-wrap { + @include display(flex); + } + &__buttons { @include display(flex); @include flex-direction(row); diff --git a/app/templates/generator-presets.hbs b/app/templates/generator-presets.hbs index 5ac44743..70f23aa9 100644 --- a/app/templates/generator-presets.hbs +++ b/app/templates/generator-presets.hbs @@ -2,27 +2,52 @@
{{res 'retToApp'}}
-

{{res 'genPsTitle'}}

- {{#if empty}} -
-

{{res 'genPsEmpty'}}

-
{{res 'genPsEmptyDesc'}}
- +
+

{{res 'genPsTitle'}}

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {{#each ranges as |range|}} +
+ +
- {{else}} - -
- - +
+ + +
+
+ +
+
+
- + {{#unless selected.builtIn}}{{/unless}}
- {{/if}}
diff --git a/app/templates/grp.hbs b/app/templates/grp.hbs index 265dc468..912c6fbc 100644 --- a/app/templates/grp.hbs +++ b/app/templates/grp.hbs @@ -16,11 +16,13 @@
{{/unless}} - {{#if customIcon}} - - {{else}} - - {{/if}} +
+ {{#if customIcon}} + + {{else}} + + {{/if}} +
{{#if canAutoType}} {{#unless readonly}} diff --git a/release-notes.md b/release-notes.md index 672c9b61..4245f591 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,6 +5,7 @@ Release notes `+` auto-type improvements `+` context menu `+` solarized themes +`+` generator presets `+` group reorder `+` select field contents on search hotkey `+` option to preload default config and file