generator presets

This commit is contained in:
antelle 2016-08-14 19:18:51 +03:00
parent e978f2adb8
commit f94493a82c
12 changed files with 362 additions and 98 deletions

View File

@ -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;

View File

@ -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',

View File

@ -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 '';
}

View File

@ -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;

View File

@ -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();
},

View File

@ -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]
});

View File

@ -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 {

View File

@ -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;
}

View File

@ -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);

View File

@ -2,27 +2,52 @@
<div class="back-button">
{{res 'retToApp'}} <i class="fa fa-external-link-square"></i>
</div>
<h1>{{res 'genPsTitle'}}</h1>
{{#if empty}}
<div class="empty-block empty-block--flex muted-color">
<h1 class="empty-block__title">{{res 'genPsEmpty'}}</h1>
<div class="gen-ps__empty-text">{{res 'genPsEmptyDesc'}}</div>
<button class="gen-ps__btn-create">{{res 'genPsCreate'}}</button>
<div class="scroller">
<h1>{{res 'genPsTitle'}}</h1>
<select class="gen-ps__list input-base">
{{#each presets as |ps|}}
<option value="{{ps.name}}" {{#ifeq ps ../selected}}selected{{/ifeq}}>{{#if ps.builtIn}}* {{/if}}{{ps.title}}</option>
{{/each}}
</select>
<div class="gen-ps__field">
<input type="checkbox" class="input-base" id="gen-ps__check-enabled" {{#unless selected.disabled}}checked{{/unless}} />
<label for="gen-ps__check-enabled">{{res 'genPsEnabled'}}</label>
</div>
<div class="gen-ps__field">
<input type="checkbox" class="input-base" id="gen-ps__check-default" {{#if selected.default}}checked{{/if}} />
<label for="gen-ps__check-default">{{res 'genPsDefault'}}</label>
</div>
<div class="gen-ps__field">
<label for="gen-ps__field-title">{{Res 'name'}}:</label>
<input type="text" class="input-base" id="gen-ps__field-title" value="{{selected.title}}"
size="50" maxlength="64" required {{#if selected.builtIn}}readonly{{/if}} />
</div>
<div class="gen-ps__field">
<label for="gen-ps__field-length">{{res 'genPsDefaultLength'}}:</label>
<input type="text" class="input-base" id="gen-ps__field-length" value="{{selected.length}}"
size="50" maxlength="3" required pattern="\d+" {{#if selected.builtIn}}readonly{{/if}} />
</div>
{{#each ranges as |range|}}
<div class="gen-ps__field">
<input type="checkbox" class="input-base gen-ps__check-range" id="gen-ps__check-{{range.name}}"
data-range="{{range.name}}"
{{#if range.enabled}}checked{{/if}} {{#if ../selected.builtIn}}disabled{{/if}} />
<label for="gen-ps__check-{{range.name}}">{{range.title}}<span class="gen-ps__sample"> {{range.sample}}</span></label>
</div>
{{else}}
<select class="gen-ps__list input-base">
{{#each presets as |ps|}}
<option value="{{ps.id}}" {{#ifeq ps ../selected}}selected{{/ifeq}}>{{ps.name}}</option>
{{/each}}
</select>
<div class="gen-ps__field">
<label for="gen-ps__field-name">{{Res 'name'}}:</label>
<input type="text" class="input-base" id="gen-ps__field-name" value="{{selected.name}}"
size="50" maxlength="64" required />
<div class="gen-ps__field">
<label for="gen-ps__field-include">{{res 'genPsInclude'}}:</label>
<input type="text" class="input-base" id="gen-ps__field-include" value="{{selected.include}}"
{{#if selected.builtIn}}readonly{{/if}} />
</div>
<div class="gen-ps__field">
<label>{{res 'genPsExample'}}:</label>
<div class="gen-ps__example"></div>
</div>
</div>
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
<div class="gen-ps__buttons">
<button class="gen-ps__btn-create">{{res 'genPsCreate'}}</button>
<button class="gen-ps__btn-delete btn-error">{{res 'genPsDelete'}}</button>
{{#unless selected.builtIn}}<button class="gen-ps__btn-delete btn-error">{{res 'genPsDelete'}}</button>{{/unless}}
</div>
{{/if}}
</div>

View File

@ -16,11 +16,13 @@
</div>
{{/unless}}
<label>{{Res 'icon'}}:</label>
{{#if customIcon}}
<img src="{{{customIcon}}}" class="grp__icon grp__icon--image" />
{{else}}
<i class="fa fa-{{icon}} grp__icon"></i>
{{/if}}
<div class="grp__icon-wrap">
{{#if customIcon}}
<img src="{{{customIcon}}}" class="grp__icon grp__icon--image" />
{{else}}
<i class="fa fa-{{icon}} grp__icon"></i>
{{/if}}
</div>
<div class="grp__icons"></div>
{{#if canAutoType}}
{{#unless readonly}}

View File

@ -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