mirror of https://github.com/keeweb/keeweb.git
Merge branch 'develop' into usb
# Conflicts: # app/scripts/const/default-app-settings.js # app/scripts/views/open-view.js
This commit is contained in:
commit
dd68aa342a
|
@ -63,7 +63,8 @@ ready(() => {
|
|||
.catch(e => {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appNotSupportedError + '<br/><br/>' + e,
|
||||
body: Locale.appNotSupportedError,
|
||||
pre: e,
|
||||
buttons: [],
|
||||
esc: false,
|
||||
enter: false,
|
||||
|
@ -147,7 +148,7 @@ ready(() => {
|
|||
esc: false,
|
||||
enter: false,
|
||||
click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
body: Locale.appSecWarnBody1 + '\n\n' + Locale.appSecWarnBody2,
|
||||
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
|
||||
complete: () => {
|
||||
showView();
|
||||
|
|
|
@ -38,10 +38,10 @@ const AppRightsChecker = {
|
|||
icon: 'lock',
|
||||
header: Locale.appRightsAlert,
|
||||
body:
|
||||
Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) +
|
||||
'<br/>' +
|
||||
Locale.appRightsAlertBody2 +
|
||||
`: <pre>${command}</pre>`,
|
||||
Locale.appRightsAlertBody1.replace('{}', this.AppPath) +
|
||||
'\n' +
|
||||
Locale.appRightsAlertBody2,
|
||||
pre: command,
|
||||
buttons: [
|
||||
{ result: 'skip', title: Locale.alertDoNotAsk, error: true },
|
||||
Alerts.buttons.ok
|
||||
|
|
|
@ -6,7 +6,6 @@ import { Otp } from 'util/data/otp';
|
|||
import { Features } from 'util/features';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { escape } from 'util/fn';
|
||||
|
||||
const logger = new Logger('otp-qr-reader');
|
||||
|
||||
|
@ -53,7 +52,7 @@ class OtpQrReader {
|
|||
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
|
||||
line3,
|
||||
Locale.detSetupOtpAlertBody4
|
||||
].join('<br/>'),
|
||||
].join('\n'),
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: '',
|
||||
|
@ -141,11 +140,8 @@ class OtpQrReader {
|
|||
logger.error('Error parsing QR code', err);
|
||||
Alerts.error({
|
||||
header: Locale.detOtpQrWrong,
|
||||
body:
|
||||
Locale.detOtpQrWrongBody +
|
||||
'<pre class="modal__pre">' +
|
||||
escape(err.toString()) +
|
||||
'</pre>'
|
||||
body: Locale.detOtpQrWrongBody,
|
||||
pre: err.toString()
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -14,6 +14,9 @@ const Launcher = {
|
|||
thirdPartyStoragesSupported: true,
|
||||
clipboardSupported: true,
|
||||
req: window.require,
|
||||
reqNative(module) {
|
||||
return this.req(`@keeweb/keeweb-native-modules/${module}.${process.platform}.node`);
|
||||
},
|
||||
platform() {
|
||||
return process.platform;
|
||||
},
|
||||
|
|
|
@ -35,6 +35,7 @@ const DefaultAppSettings = {
|
|||
allowIframes: false, // allow displaying the app in IFrames
|
||||
useGroupIconForEntries: false, // automatically use group icon when creating new entries
|
||||
enableUsb: true, // enable interaction with USB devices
|
||||
nativeArgon2: true, // use native argon2 module
|
||||
|
||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||
yubiKeyAutoOpen: true, // auto-load one-time codes when there are open files
|
||||
|
|
|
@ -522,6 +522,8 @@
|
|||
"setFileCloseNoSave": "Close and lose changes",
|
||||
"setFileDontClose": "Don't close",
|
||||
"setFileFormatVersion": "File format",
|
||||
"saveFileExportRaw": "Exporting your passwords",
|
||||
"saveFileExportRawBody": "The exported file will contain your passwords, they will not be encrypted there. Would you like to proceed?",
|
||||
|
||||
"setShTitle": "Shortcuts",
|
||||
"setShShowAll": "show all items",
|
||||
|
|
|
@ -409,7 +409,9 @@ class AppModel {
|
|||
newEntry.copyFromTemplate(templateEntry);
|
||||
return newEntry;
|
||||
} else {
|
||||
return EntryModel.newEntry(sel.group, sel.file);
|
||||
return EntryModel.newEntry(sel.group, sel.file, {
|
||||
tag: this.filter.tag
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -792,12 +792,15 @@ class EntryModel extends Model {
|
|||
return model;
|
||||
}
|
||||
|
||||
static newEntry(group, file) {
|
||||
static newEntry(group, file, opts) {
|
||||
const model = new EntryModel();
|
||||
const entry = file.db.createEntry(group.group);
|
||||
if (AppSettingsModel.useGroupIconForEntries && group.icon && group.iconId) {
|
||||
entry.icon = group.iconId;
|
||||
}
|
||||
if (opts && opts.tag) {
|
||||
entry.tags = [opts.tag];
|
||||
}
|
||||
model.setEntry(entry, group, file);
|
||||
model.entry.times.update();
|
||||
model.unsaved = true;
|
||||
|
|
|
@ -13,7 +13,14 @@ class StorageWebDav extends StorageBase {
|
|||
getOpenConfig() {
|
||||
return {
|
||||
fields: [
|
||||
{ id: 'path', title: 'openUrl', desc: 'openUrlDesc', type: 'text', required: true },
|
||||
{
|
||||
id: 'path',
|
||||
title: 'openUrl',
|
||||
desc: 'openUrlDesc',
|
||||
type: 'text',
|
||||
required: true,
|
||||
pattern: '^https://.+'
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
title: 'openUser',
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Features } from 'util/features';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
|
||||
const logger = new Logger('argon2');
|
||||
|
||||
|
@ -27,6 +29,40 @@ const KdbxwebInit = {
|
|||
if (!global.WebAssembly) {
|
||||
return Promise.reject('WebAssembly is not supported');
|
||||
}
|
||||
if (Launcher && Launcher.reqNative && AppSettingsModel.nativeArgon2) {
|
||||
const ts = logger.ts();
|
||||
const argon2 = Launcher.reqNative('argon2');
|
||||
logger.debug('Native argon2 runtime loaded (main thread)', logger.ts(ts));
|
||||
this.runtimeModule = {
|
||||
hash(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ts = logger.ts();
|
||||
argon2.hash(
|
||||
Buffer.from(args.password),
|
||||
Buffer.from(args.salt),
|
||||
{
|
||||
type: args.type,
|
||||
version: args.version,
|
||||
hashLength: args.length,
|
||||
saltLength: args.salt.length,
|
||||
timeCost: args.iterations,
|
||||
parallelism: args.parallelism,
|
||||
memoryCost: args.memory
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
logger.error('Argon2 error', err);
|
||||
return reject(err);
|
||||
}
|
||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
||||
resolve(res);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
return Promise.resolve(this.runtimeModule);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const loadTimeout = setTimeout(() => reject('timeout'), 5000);
|
||||
try {
|
||||
|
|
|
@ -1004,11 +1004,8 @@ class DetailsView extends View {
|
|||
deleteFromTrash() {
|
||||
Alerts.yesno({
|
||||
header: Locale.detDelFromTrash,
|
||||
body:
|
||||
Locale.detDelFromTrashBody +
|
||||
' <p class="muted-color">' +
|
||||
Locale.detDelFromTrashBodyHint +
|
||||
'</p>',
|
||||
body: Locale.detDelFromTrashBody,
|
||||
hint: Locale.detDelFromTrashBodyHint,
|
||||
icon: 'minus-circle',
|
||||
success: () => {
|
||||
this.model.deleteFromTrash();
|
||||
|
|
|
@ -22,8 +22,8 @@ class FieldViewUrl extends FieldViewText {
|
|||
}
|
||||
|
||||
fixUrl(url) {
|
||||
const proto = new URL(url, 'dummy://').protocol;
|
||||
if (proto === 'dummy:') {
|
||||
const proto = new URL(url, 'ws://x').protocol;
|
||||
if (proto === 'ws:') {
|
||||
return 'https://' + url;
|
||||
}
|
||||
if (!AllowedProtocols.includes(proto)) {
|
||||
|
@ -37,7 +37,7 @@ class FieldViewUrl extends FieldViewText {
|
|||
}
|
||||
|
||||
getTextValue() {
|
||||
return this.fixUrl(this.value);
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -123,11 +123,12 @@ class ImportCsvView extends View {
|
|||
this.groups = [];
|
||||
for (const file of this.appModel.files) {
|
||||
file.forEachGroup(group => {
|
||||
let title = escape(group.title);
|
||||
const title = group.title;
|
||||
const spaces = [];
|
||||
for (let parent = group; parent.parentGroup; parent = parent.parentGroup) {
|
||||
title = ' ' + title;
|
||||
spaces.push(' ', ' ');
|
||||
}
|
||||
this.groups.push({ id: group.id, fileId: file.id, title });
|
||||
this.groups.push({ id: group.id, fileId: file.id, spaces, title });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,15 +137,15 @@ class ListSearchView extends View {
|
|||
this.sortOptions.forEach(opt => {
|
||||
opt.html = opt.loc();
|
||||
});
|
||||
const entryDesc = Features.isMobile
|
||||
? ''
|
||||
: ' <span class="muted-color">(' +
|
||||
Locale.searchShiftClickOr +
|
||||
' ' +
|
||||
Shortcuts.altShortcutSymbol(true) +
|
||||
'N)</span>';
|
||||
this.createOptions = [
|
||||
{ value: 'entry', icon: 'key', html: StringFormat.capFirst(Locale.entry) + entryDesc },
|
||||
{
|
||||
value: 'entry',
|
||||
icon: 'key',
|
||||
text: StringFormat.capFirst(Locale.entry),
|
||||
hint: Features.isMobile
|
||||
? null
|
||||
: `(${Locale.searchShiftClickOr} ${Shortcuts.altShortcutSymbol(true)})`
|
||||
},
|
||||
{ value: 'group', icon: 'folder', text: StringFormat.capFirst(Locale.group) }
|
||||
];
|
||||
if (this.el) {
|
||||
|
|
|
@ -105,7 +105,7 @@ class ListView extends View {
|
|||
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
|
||||
}, this);
|
||||
const html = itemsTemplate(
|
||||
{ items: itemsHtml, columns: this.tableColumns },
|
||||
{ itemsHtml, columns: this.tableColumns },
|
||||
DefaultTemplateOptions
|
||||
);
|
||||
this.itemsEl.html(html);
|
||||
|
@ -123,8 +123,8 @@ class ListView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
renderPlainItems(itemsHtml) {
|
||||
return itemsHtml.items;
|
||||
renderPlainItems(data) {
|
||||
return data.itemsHtml;
|
||||
}
|
||||
|
||||
getItemTemplate() {
|
||||
|
@ -180,13 +180,13 @@ class ListView extends View {
|
|||
}
|
||||
|
||||
createTemplate() {
|
||||
if (!this.model.settings.templateHelpShown) {
|
||||
if (!this.model.settings.templateHelpShown_) {
|
||||
Alerts.yesno({
|
||||
icon: 'sticky-note-o',
|
||||
header: Locale.listAddTemplateHeader,
|
||||
body:
|
||||
Locale.listAddTemplateBody1.replace('{}', '<i class="fa fa-plus"></i>') +
|
||||
'<br/>' +
|
||||
Locale.listAddTemplateBody1.replace('{}', '"+"') +
|
||||
'\n' +
|
||||
Locale.listAddTemplateBody2.replace('{}', 'Templates'),
|
||||
buttons: [Alerts.buttons.ok, Alerts.buttons.cancel],
|
||||
success: () => {
|
||||
|
|
|
@ -29,7 +29,10 @@ class ModalView extends View {
|
|||
}
|
||||
|
||||
render() {
|
||||
super.render(this.model);
|
||||
super.render({
|
||||
...this.model,
|
||||
body: this.model.body ? this.model.body.toString().split('\n') : ''
|
||||
});
|
||||
this.$el.addClass('modal--hidden');
|
||||
setTimeout(() => {
|
||||
this.$el.removeClass('modal--hidden');
|
||||
|
@ -43,7 +46,7 @@ class ModalView extends View {
|
|||
|
||||
change(config) {
|
||||
if (config.header) {
|
||||
this.$el.find('.modal__header').html(config.header);
|
||||
this.$el.find('.modal__header').text(config.header);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Logger } from 'util/logger';
|
|||
import { InputFx } from 'util/ui/input-fx';
|
||||
import { OpenConfigView } from 'views/open-config-view';
|
||||
import { StorageFileListView } from 'views/storage-file-list-view';
|
||||
import { escape, omit } from 'util/fn';
|
||||
import { omit } from 'util/fn';
|
||||
import { GeneratorView } from 'views/generator-view';
|
||||
import template from 'templates/open.hbs';
|
||||
|
||||
|
@ -77,6 +77,7 @@ class OpenView extends View {
|
|||
this.once('remove', () => {
|
||||
this.passwordInput.reset();
|
||||
});
|
||||
this.listenTo(Events, 'user-idle', this.userIdle);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -674,11 +675,8 @@ class OpenView extends View {
|
|||
}
|
||||
Alerts.error({
|
||||
header: Locale.openError,
|
||||
body:
|
||||
Locale.openErrorDescription +
|
||||
'<pre class="modal__pre">' +
|
||||
escape(err.toString()) +
|
||||
'</pre>'
|
||||
body: Locale.openErrorDescription,
|
||||
pre: err.toString()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -757,11 +755,8 @@ class OpenView extends View {
|
|||
if (err.lastIndexOf('OAuth', 0) !== 0 && !Alerts.alertDisplayed) {
|
||||
Alerts.error({
|
||||
header: Locale.openError,
|
||||
body:
|
||||
Locale.openListErrorBody +
|
||||
'<pre class="modal__pre">' +
|
||||
escape(err.toString()) +
|
||||
'</pre>'
|
||||
body: Locale.openListErrorBody,
|
||||
pre: err.toString()
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
@ -979,6 +974,11 @@ class OpenView extends View {
|
|||
this.views.gen = generator;
|
||||
}
|
||||
|
||||
userIdle() {
|
||||
this.inputEl.val('');
|
||||
this.passwordInput.reset();
|
||||
}
|
||||
|
||||
usbDevicesChanged() {
|
||||
if (this.model.settings.canOpenOtpDevice && this.model.settings.yubiKeyShowIcon) {
|
||||
const hasYubiKeys = !!UsbListener.attachedYubiKeys.length;
|
||||
|
|
|
@ -12,7 +12,7 @@ import { PasswordPresenter } from 'util/formatting/password-presenter';
|
|||
import { Locale } from 'util/locale';
|
||||
import { FileSaver } from 'util/ui/file-saver';
|
||||
import { OpenConfigView } from 'views/open-config-view';
|
||||
import { escape, omit } from 'util/fn';
|
||||
import { omit } from 'util/fn';
|
||||
import template from 'templates/settings/settings-file.hbs';
|
||||
|
||||
const DefaultBackupPath = 'Backups/{name}.{date}.bak';
|
||||
|
@ -240,8 +240,8 @@ class SettingsFileView extends View {
|
|||
if (err) {
|
||||
Alerts.error({
|
||||
header: Locale.setFileSaveError,
|
||||
body:
|
||||
Locale.setFileSaveErrorBody + ' ' + path + ': \n' + err
|
||||
body: Locale.setFileSaveErrorBody + ' ' + path + ':',
|
||||
pre: err
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -256,16 +256,28 @@ class SettingsFileView extends View {
|
|||
}
|
||||
|
||||
saveToXml() {
|
||||
this.model.getXml(xml => {
|
||||
const blob = new Blob([xml], { type: 'text/xml' });
|
||||
FileSaver.saveAs(blob, this.model.name + '.xml');
|
||||
Alerts.yesno({
|
||||
header: Locale.saveFileExportRaw,
|
||||
body: Locale.saveFileExportRawBody,
|
||||
success: () => {
|
||||
this.model.getXml(xml => {
|
||||
const blob = new Blob([xml], { type: 'text/xml' });
|
||||
FileSaver.saveAs(blob, this.model.name + '.xml');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveToHtml() {
|
||||
this.model.getHtml(html => {
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
FileSaver.saveAs(blob, this.model.name + '.html');
|
||||
Alerts.yesno({
|
||||
header: Locale.saveFileExportRaw,
|
||||
body: Locale.saveFileExportRawBody,
|
||||
success: () => {
|
||||
this.model.getHtml(html => {
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
FileSaver.saveAs(blob, this.model.name + '.html');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -332,10 +344,7 @@ class SettingsFileView extends View {
|
|||
if (existingFile) {
|
||||
Alerts.yesno({
|
||||
header: Locale.setFileAlreadyExists,
|
||||
body: Locale.setFileAlreadyExistsBody.replace(
|
||||
'{}',
|
||||
this.model.escape('name')
|
||||
),
|
||||
body: Locale.setFileAlreadyExistsBody.replace('{}', this.model.name),
|
||||
success: () => {
|
||||
this.model.syncing = true;
|
||||
storage.remove(existingFile.path, err => {
|
||||
|
@ -587,11 +596,8 @@ class SettingsFileView extends View {
|
|||
}
|
||||
Alerts.error({
|
||||
title,
|
||||
body:
|
||||
description +
|
||||
'<pre class="modal__pre">' +
|
||||
escape(err.toString()) +
|
||||
'</pre>'
|
||||
body: description,
|
||||
pre: err.toString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { View } from 'framework/views/view';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Links } from 'const/links';
|
||||
import { escape } from 'util/fn';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import template from 'templates/settings/settings-help.hbs';
|
||||
|
||||
|
@ -30,7 +29,7 @@ class SettingsHelpView extends View {
|
|||
encodeURIComponent('!please describe your issue here!\n\n' + appInfo),
|
||||
desktopLink: Links.Desktop,
|
||||
webAppLink: Links.WebApp,
|
||||
appInfo: escape(appInfo)
|
||||
appInfo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
id="at-select__item--{{id}}">
|
||||
<td>
|
||||
{{~#if customIcon~}}
|
||||
<img src="{{{customIcon}}}" class="at-select__item-icon at-select__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
<img src="{{customIcon}}" class="at-select__item-icon at-select__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
{{~else~}}
|
||||
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} at-select__item-icon"></i>
|
||||
{{~/if~}}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<h1 class="details__header-title">{{#if title}}{{title}}{{else}}(no title){{/if}}</h1>
|
||||
{{#if customIcon}}
|
||||
<div class="details__header-icon details__header-icon--icon" title="{{res 'detSetIcon'}}">
|
||||
<img class="details__header-icon-img" src="{{{customIcon}}}" />
|
||||
<img class="details__header-icon-img" src="{{customIcon}}" />
|
||||
</div>
|
||||
{{else}}
|
||||
<i class="details__header-icon fa fa-{{icon}}" title="{{res 'detSetIcon'}}"></i>
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
<div class="dropdown__item {{#if option.active}}dropdown__item--active{{/if}}" data-value="{{option.value}}">
|
||||
<i class="fa fa-{{option.icon}} dropdown__item-icon"></i>
|
||||
<span class="dropdown__item-text">
|
||||
{{~#if option.text~}}
|
||||
{{option.text}}
|
||||
{{~else~}}
|
||||
{{{option.html}}}
|
||||
{{option.text}}
|
||||
{{~#if option.hint~}}
|
||||
<span class="muted-color">{{option.hint}}</span>
|
||||
{{~/if~}}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<label>{{Res 'icon'}}:</label>
|
||||
<div class="grp__icon-wrap">
|
||||
{{#if customIcon}}
|
||||
<img src="{{{customIcon}}}" class="grp__icon grp__icon--image" />
|
||||
<img src="{{customIcon}}" class="grp__icon grp__icon--image" />
|
||||
{{else}}
|
||||
<i class="fa fa-{{icon}} grp__icon"></i>
|
||||
{{/if}}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
{{#each customIcons as |icon ci|}}
|
||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci ../sel}}icon-select__icon--active{{/ifeq}}"
|
||||
data-val="{{ci}}">
|
||||
<img src="{{{icon}}}" />
|
||||
<img src="{{icon}}" />
|
||||
</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
|
|
@ -57,7 +57,10 @@
|
|||
<select class="import-csv__target-select" id="import-csv__target-select">
|
||||
<option value="" selected>{{res 'importNewFile'}}</option>
|
||||
{{#each groups as |group|}}
|
||||
<option value="{{group.id}}" data-file="{{group.fileId}}">{{{group.title}}}</option>
|
||||
<option value="{{group.id}}" data-file="{{group.fileId}}">
|
||||
{{#if spaces}}{{#each spaces}} {{/each}}{{/if}}
|
||||
{{group.title}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="list__item {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
|
||||
{{#if customIcon~}}
|
||||
<img src="{{{customIcon}}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
<img src="{{customIcon}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
{{~else~}}
|
||||
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
|
||||
{{~/if}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<tr class="list__item list__item--table {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
|
||||
<td>
|
||||
{{~#if customIcon~}}
|
||||
<img src="{{{customIcon}}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
<img src="{{customIcon}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
|
||||
{{~else~}}
|
||||
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
|
||||
{{~/if~}}
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{items}}}
|
||||
{{{itemsHtml}}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div class="menu__item-body" {{#if drag}}draggable="true"{{/if}}>
|
||||
{{#if drag}}<div class="menu__item-drag-top"></div>{{/if}}
|
||||
{{#if customIcon~}}
|
||||
<img src="{{{customIcon}}}" class="menu__item-icon menu__item-icon--image" />
|
||||
<img src="{{customIcon}}" class="menu__item-icon menu__item-icon--image" />
|
||||
{{~else~}}
|
||||
<i class="menu__item-icon fa {{#if icon}}fa-{{icon}}{{else}}menu__item-icon--no-icon{{/if}}"></i>
|
||||
{{~/if}}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
<div class="modal modal--hidden {{#if opaque}}modal--opaque{{/if}}">
|
||||
<div class="modal__content">
|
||||
<i class="modal__icon fa fa-{{icon}}"></i>
|
||||
<div class="modal__header">{{{header}}}</div>
|
||||
<div class="modal__header">{{header}}</div>
|
||||
<div class="modal__body">
|
||||
{{{body}}}
|
||||
{{#each body as |item|}}
|
||||
{{item}}
|
||||
{{#unless @last}}<br/>{{/unless}}
|
||||
{{/each}}
|
||||
{{#if pre}}<pre class="modal__pre">{{pre}}</pre>{{/if}}
|
||||
{{#if hint}}<p class="muted-color">{{hint}}</p>{{/if}}
|
||||
{{#if checkbox}}
|
||||
<div class="modal__check-wrap"><input type="checkbox" id="modal__check" /><label for="modal__check">{{checkbox}}</label></div>
|
||||
{{/if}}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
autocomplete="off" autocapitalize="off"
|
||||
{{#if placeholder}}placeholder="{{res placeholder}}"{{/if}}
|
||||
{{#if required}}required{{/if}}
|
||||
{{#if pattern}}pattern="{{{pattern}}}"{{/if}}
|
||||
{{#if pattern}}pattern="{{pattern}}"{{/if}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<h3>Desktop modules</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/antelle/node-stream-zip" target="_blank">node-stream-zip</a><span class="muted-color">, node.js library for fast reading of large ZIPs</span></li>
|
||||
<li><a href="https://github.com/ranisalt/node-argon2" target="_blank">node-argon2</a><span class="muted-color">, node.js bindings for Argon2 hashing algorithm</span></li>
|
||||
</ul>
|
||||
|
||||
<h3>Utils</h3>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
data-id="{{id}}"
|
||||
{{#if placeholder}}placeholder="{{res placeholder}}"{{/if}}
|
||||
{{#if required}}required{{/if}}
|
||||
{{#if pattern}}pattern="{{{pattern}}}"{{/if}}
|
||||
{{#if pattern}}pattern="{{pattern}}"{{/if}}
|
||||
/>
|
||||
{{/ifeq}}
|
||||
{{/each}}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
Release notes
|
||||
-------------
|
||||
##### v1.15.0 (WIP)
|
||||
`+` #557: Argon2 speed improvements in desktop apps
|
||||
`+` #1400: auto-apply tag when creating new entry in tag view
|
||||
`+` #1342: hint that the data will be stored in unencrypted form after exporting
|
||||
`*` #1471: WebDAV url validation, only HTTPS is allowed
|
||||
`+` #1350: clearing master password after auto lock period
|
||||
`-` fix #1463: copying the original url instead of adding https:
|
||||
|
||||
##### v1.14.0 (2020-04-18)
|
||||
`+` using OAuth authorization code grant for all storage providers
|
||||
`-` fixed a number of vulnerabilities in opening untrusted kdbx files
|
||||
|
|
Loading…
Reference in New Issue