diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index 8637bf5..8c0033d 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -23,6 +23,10 @@ "message": "Save credentials", "description": "Save credentials using the extension popup icon." }, + "contextMenuShowPasswordGenerator": { + "message": "Show password generator", + "description": "Show password generator text." + }, "rememberCredentialsPopup": { "message": "Create or modify the credentials by clicking on the extension icon.", "description": "Notification text when saving credentials from the context menu." @@ -95,6 +99,10 @@ "message": "No logins found.", "description": "No logins found." }, + "passwordGeneratorIcon": { + "message": "Password generator icon", + "description": "Alt attribute text of the password generator icon." + }, "passwordGeneratorPlaceholder": { "message": "Generated password", "description": "Input field placeholder for generated password." @@ -183,6 +191,10 @@ "message": "Please confirm your selection or choose more fields as String fields.", "description": "Confirm a selection text when choosing custom login fields which contains string fields." }, + "defineKeyboardText": { + "message": "You can also use the numbers to choose the input fields from keyboard.", + "description": "Help text when choosing custom login fields." + }, "username": { "message": "Username", "description": "General text for username." @@ -707,6 +719,10 @@ "message": "No saved custom login fields found.", "description": "Shown when no saved custom credentials are saved." }, + "optionsCustomFieldsTableCaption": { + "message": "You can manage saved custom login fields using the table below.", + "description": "Table caption of Custom login fields page." + }, "optionsCustomFieldsTabHelpTextFirst": { "message": "If KeePassXC-Browser detects the wrong login fields, you are able to specify the correct fields yourself.", "description": "Saved custom login fields info text, first part." @@ -715,10 +731,6 @@ "message": "Go to the page and click on the KeePassXC-Browser icon, then select ", "description": "Saved custom login fields info text, second part." }, - "optionsCustomFieldsTabHelpTextThird": { - "message": "On this page you can manage saved custom login fields.", - "description": "Saved custom login fields info text, third part." - }, "optionsCustomFieldsRemove": { "message": "Remove saved custom login fields?", "description": "Confirmation text when removing saved custom login fields." @@ -756,7 +768,7 @@ "description": "Site preferences info text, fourth part." }, "optionsSitePreferencesManualAddText": { - "message": "Add URL manually:", + "message": "Add URL manually", "description": "Label for adding site manually on Site preferences tab." }, "optionsSitePreferencesConfirmation": { @@ -767,6 +779,14 @@ "message": "KeePassXC-Browser will enable all features for this site and remove username-only detection.", "description": "Part of confirmation text when removing site preferences." }, + "optionsSitePreferencesTableCaption": { + "message": "You can manage saved site preferences using the table below.", + "description": "Table caption of Site Preferences page." + }, + "optionsSitePreferencesSelect": { + "message": "Option to ignore extension features for the page.", + "description": "Title text of select element in Site Preferences." + }, "optionsAboutChrome": { "message": "Visit KeePassXC-Browser in the Chrome Web Store.", "description": "About page link to Chrome Web Store." @@ -788,11 +808,43 @@ "description": "About page text of KeePassXC version." }, "optionsAboutContributors": { - "message": "Contributors:", + "message": "Contributors", "description": "About page text of contributors." }, "optionsAboutLicenses": { - "message": "Licenses:", + "message": "Licenses", "description": "About page text of licenses." + }, + "optionsAboutReportBugs": { + "message": "Report bugs", + "description": "About page text of reporting bugs." + }, + "optionsAboutCreateNewIssue": { + "message": "Create new issue on GitHub", + "description": "Link text for creating a new issue." + }, + "optionsAboutDebugInfo": { + "message": "The following debug info can be used", + "description": "Debug info text." + }, + "optionsErrorInvalidValue": { + "message": "Invalid value. Please check the input.", + "description": "Error shown when input field has an incorrect value." + }, + "optionsErrorInvalidURL": { + "message": "Invalid URL. Please check the input.", + "description": "Error shown when input field has an incorrect URL." + }, + "optionsErrorValueExists": { + "message": "The value already exists in the list.", + "description": "Error shown when trying to add a duplicate value to the list." + }, + "openNewTab": { + "message": "Opens a new tab", + "description": "Title attribute text." + }, + "httpAuthDialog": { + "message": "HTTP Basic Authentication dialog", + "description": "Options page HTTP Basic Auth dialog text" } } diff --git a/keepassxc-browser/background/init.js b/keepassxc-browser/background/init.js index f4797c5..c603f9f 100644 --- a/keepassxc-browser/background/init.js +++ b/keepassxc-browser/background/init.js @@ -81,11 +81,12 @@ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { browser.runtime.onMessage.addListener(kpxcEvent.onMessage); const contextMenuItems = [ - {title: tr('contextMenuFillUsernameAndPassword'), action: 'fill_username_password'}, - {title: tr('contextMenuFillPassword'), action: 'fill_password'}, - {title: tr('contextMenuFillTOTP'), action: 'fill_totp'}, - {title: tr('contextMenuShowPasswordGeneratorIcons'), action: 'activate_password_generator'}, - {title: tr('contextMenuSaveCredentials'), action: 'remember_credentials'} + { title: tr('contextMenuFillUsernameAndPassword'), action: 'fill_username_password' }, + { title: tr('contextMenuFillPassword'), action: 'fill_password' }, + { title: tr('contextMenuFillTOTP'), action: 'fill_totp' }, + { title: tr('contextMenuShowPasswordGeneratorIcons'), action: 'activate_password_generator' }, + { title: tr('contextMenuSaveCredentials'), action: 'remember_credentials' }, + { title: tr('contextMenuShowPasswordGenerator'), action: 'show_password_generator' } ]; const menuContexts = ['editable']; diff --git a/keepassxc-browser/keepassxc-browser.css b/keepassxc-browser/keepassxc-browser.css index 9c3b0f7..6e55f77 100644 --- a/keepassxc-browser/keepassxc-browser.css +++ b/keepassxc-browser/keepassxc-browser.css @@ -117,15 +117,15 @@ input.genpw-text { } .b2c-modal-backdrop:after { - content:''; + content: ''; position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: #000000; - opacity: 0.8; - filter: alpha(opacity=80); + opacity: 0.5; + filter: alpha(opacity=50); } #b2c-cipDefine-fields { @@ -142,7 +142,7 @@ input.genpw-text { left: 150px; text-align: left; cursor: pointer; - background-color:rgba(255,255,255,0.3); + background-color: rgba(0,0,0,0.8); font-size: 15px; } @@ -168,12 +168,28 @@ input.genpw-text { margin-bottom: 3px; } +#b2c-keyboardHelp { + font-size: 0.75em; +} + +#b2c-keyboardHelp kbd { + background-color: #eee; + border-radius: 3px; + border: 1px solid #b4b4b4; + color: #333; + display: inline-block; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; +} + .b2c-fixed-field { position: absolute; border: 2px solid #efefef; + color: #fff; cursor: pointer; z-index: 2147483646; - text-align: left; + text-align: center; font-weight: bold; background-color:rgba(239,239,239,0.4); } diff --git a/keepassxc-browser/keepassxc-browser.js b/keepassxc-browser/keepassxc-browser.js index bceb3c7..5f53381 100755 --- a/keepassxc-browser/keepassxc-browser.js +++ b/keepassxc-browser/keepassxc-browser.js @@ -37,21 +37,21 @@ browser.runtime.onMessage.addListener(function(req, sender, callback) { cip.setValueWithChange(cip.u, cip.credentials[req.id].login); combination = cipFields.getCombination('username', cip.u); browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [req.id] + action: 'page_set_login_id', args: [ req.id ] }); cip.u.focus(); } if (cip.p) { cip.setValueWithChange(cip.p, cip.credentials[req.id].password); browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [req.id] + action: 'page_set_login_id', args: [ req.id ] }); combination = cipFields.getCombination('password', cip.p); } let list = []; if (cip.fillInStringFields(combination.fields, cip.credentials[req.id].stringFields, list)) { - cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); + cipForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); } } } else if (req.action === 'fill_username_password') { @@ -89,9 +89,10 @@ browser.runtime.onMessage.addListener(function(req, sender, callback) { }); } else if (req.action === 'ignore-site') { cip.ignoreSite(req.args); - } - else if (req.action === 'check_database_hash' && 'hash' in req) { + } else if (req.action === 'check_database_hash' && 'hash' in req) { cip.detectDatabaseChange(req.hash); + } else if (req.action === 'show_password_generator') { + cipPassword.showDialog(); } } }); @@ -108,7 +109,7 @@ cipAutocomplete.init = function(field) { } const acMenu = jQuery('#kpxc-ac-menu'); - if (acMenu.length == 0) { + if (acMenu.length === 0) { jQuery('
').appendTo('body'); } @@ -263,6 +264,7 @@ cipPassword.createDialog = function() { .attr('type', 'text') .attr('aria-describedby', 'cip-genpw-quality') .attr('placeholder', tr('passwordGeneratorPlaceholder')) + .attr('tabindex', -1) .addClass('genpw-text ui-widget-content ui-corner-all') .on('change keypress paste textInput input', function() { jQuery('#cip-genpw-btn-clipboard:first').removeClass('btn-success'); @@ -392,6 +394,7 @@ cipPassword.createIcon = function(field) { .addClass('cip-genpw-icon') .addClass($className) .attr('title', tr('passwordGeneratorGenerateText')) + .attr('alt', tr('passwordGeneratorIcon')) .css('z-index', '9999') .css('width', $size) .css('height', $size) @@ -431,6 +434,10 @@ cipPassword.createIcon = function(field) { jQuery('body').append($icon); }; +cipPassword.showDialog = function() { + $('.cip-genpw-icon').click(); +}; + cipPassword.setIconPosition = function($icon, $field) { $icon.css('top', $field.offset().top + $icon.data('offset') + 1) .css('left', $field.offset().left + $field.outerWidth() - $icon.data('size') - $icon.data('offset')); @@ -579,6 +586,7 @@ cipDefine.selection = { fields: [] }; cipDefine.eventFieldClick = null; +cipDefine.keyDown = null; cipDefine.init = function() { const $backdrop = jQuery('
').attr('id', 'b2c-backdrop').addClass('b2c-modal-backdrop'); @@ -598,6 +606,14 @@ cipDefine.init = function() { cipDefine.resetSelection(); cipDefine.prepareStep1(); cipDefine.markAllUsernameFields($chooser); + + document.addEventListener('keydown', cipDefine.keyDown); +}; + +cipDefine.close = function() { + $('div#b2c-backdrop').remove(); + $('div#b2c-cipDefine-fields').remove(); + document.removeEventListener('keydown', cipDefine.keyDown); }; cipDefine.initDescription = function() { @@ -606,85 +622,44 @@ cipDefine.initDescription = function() { $description.append($h1); const $help = jQuery('
').addClass('b2c-chooser-help').attr('id', 'b2c-help'); $description.append($help); + const $keyboardHelp = jQuery('
').addClass('b2c-chooser-help').attr('id', 'b2c-keyboardHelp'); + $description.append($keyboardHelp); + + // Show keyboard shortcuts help text + jQuery('div#b2c-keyboardHelp').text(`${tr('optionsKeyboardShortcutsHeader')}:`).css('margin-bottom', '5px'); + jQuery('div#b2c-keyboardHelp').append($('
Escape')).append(' ' + tr('defineDismiss')); + jQuery('div#b2c-keyboardHelp').append($('
S')).append(' ' + tr('defineSkip')); + jQuery('div#b2c-keyboardHelp').append($('
A')).append(' ' + tr('defineAgain')); + jQuery('div#b2c-keyboardHelp').append($('
C')).append(' ' + tr('defineConfirm')); + jQuery('div#b2c-keyboardHelp').append($('
M')).append(' ' + tr('defineMore')); + jQuery('div#b2c-keyboardHelp').append($('
D')).append(' ' + tr('defineDiscard')); const $btnDismiss = jQuery('

+

- +
@@ -58,7 +60,7 @@
- +
@@ -76,7 +78,7 @@
- +
@@ -114,7 +116,7 @@

@@ -126,7 +128,7 @@

@@ -135,7 +137,7 @@

@@ -145,7 +147,7 @@

@@ -154,13 +156,13 @@

@@ -168,7 +170,7 @@

@@ -176,10 +178,10 @@

- - - - + + + +

@@ -192,14 +194,14 @@
- +

@@ -207,7 +209,8 @@
- http-auth-dialog + +

@@ -215,15 +218,15 @@


-

+ - - - - - + + + + + @@ -257,7 +260,7 @@ @@ -272,14 +275,13 @@
. -
-

+ - - + + @@ -305,7 +307,7 @@ @@ -327,22 +329,23 @@


- -
+ +
- +

+ - - - - + + + + @@ -352,13 +355,13 @@ - + @@ -376,7 +379,7 @@ @@ -388,13 +391,13 @@


- +

- +

- . +


@@ -405,14 +408,16 @@


-
- Perry Nguyen, Lukas Schulze, - Andy Brandt, Sami Vänttinen, - KeePassXC Team. + :
+ Perry Nguyen, + Lukas Schulze, + Andy Brandt, + Sami Vänttinen, + KeePassXC Team.


-
+ :

Bootstrap @@ -569,13 +574,22 @@

+
+

+ :
+ +

: +

KeePassXC - +
KeePassXC-Browser - +
Operating system: +
Browser: +

+


- - diff --git a/keepassxc-browser/options/options.js b/keepassxc-browser/options/options.js index 3332a97..8936a74 100644 --- a/keepassxc-browser/options/options.js +++ b/keepassxc-browser/options/options.js @@ -23,6 +23,9 @@ $(function() { options.initAbout(); }); }); + + // Set options page language to HTML element from locale + $('html').attr('lang', browser.i18n.getUILanguage()); }); var options = options || {}; @@ -140,7 +143,9 @@ options.initGeneralSettings = function() { browser.commands.getAll().then(function(commands) { commands.forEach(function(command) { var shortcut = document.getElementById(`${command.name}-shortcut`); - if (!shortcut) return; + if (!shortcut) { + return; + } shortcut.textContent = command.shortcut || 'not configured'; }); }); @@ -152,7 +157,12 @@ options.initGeneralSettings = function() { }); $('#blinkTimeoutButton').click(function() { - const blinkTimeout = $.trim($('#blinkTimeout').val()); + const input = document.querySelector('#blinkTimeout'); + if (!input.validity.valid) { + options.createWarning(input, tr('optionsErrorInvalidValue')); + } + + const blinkTimeout = input.value; const blinkTimeoutval = blinkTimeout !== '' ? Number(blinkTimeout) : defaultSettings.blinkTimeout; options.settings['blinkTimeout'] = blinkTimeoutval; @@ -200,6 +210,7 @@ options.showKeePassXCVersions = function(response) { $('#tab-general-settings .kphVersion:first em.yourVersion:first').text(response.current); $('#tab-general-settings .kphVersion:first em.latestVersion:first').text(response.latest); $('#tab-about em.versionKPH').text(response.current); + $('#tab-about span.kpxcVersion').text(response.current); $('#tab-general-settings button.checkUpdateKeePassXC:first').attr('disabled', false); }; @@ -214,6 +225,9 @@ options.initConnectedDatabases = function() { $('#dialogDeleteConnectedDatabase').data('hash', $(this).closest('tr').data('hash')); $('#dialogDeleteConnectedDatabase .modal-body:first span:first').text($(this).closest('tr').children('td:first').text()); $('#dialogDeleteConnectedDatabase').modal('show'); + $('#dialogDeleteConnectedDatabase').on('shown.bs.modal', () => { + $('#dialogDeleteConnectedDatabase').find('[autofocus]').focus(); + }); }); $('#dialogDeleteConnectedDatabase .modal-footer:first button.yes:first').click(function(e) { @@ -273,6 +287,9 @@ options.initCustomCredentialFields = function() { $('#dialogDeleteCustomCredentialFields').data('tr-id', $(this).closest('tr').attr('id')); $('#dialogDeleteCustomCredentialFields .modal-body:first strong:first').text($(this).closest('tr').children('td:first').text()); $('#dialogDeleteCustomCredentialFields').modal('show'); + $('#dialogDeleteCustomCredentialFields').on('shown.bs.modal', () => { + $('#dialogDeleteCustomCredentialFields').find('[autofocus]').focus(); + }); }); $('#dialogDeleteCustomCredentialFields .modal-footer:first button.yes:first').click(function(e) { @@ -320,6 +337,9 @@ options.initSitePreferences = function() { $('#dialogDeleteSite').data('tr-id', $(this).closest('tr').attr('id')); $('#dialogDeleteSite .modal-body:first strong:first').text($(this).closest('tr').children('td:first').text()); $('#dialogDeleteSite').modal('show'); + $('#dialogDeleteSite').on('shown.bs.modal', () => { + $('#dialogDeleteSite').find('[autofocus]').focus(); + }); }); $('#tab-site-preferences tr.clone:first input[type=checkbox]:first').change(function() { @@ -343,15 +363,37 @@ options.initSitePreferences = function() { }); $('#manualUrl').keyup(function(event) { - if (event.keyCode === 13) { + if (event.key === 'Enter') { $('#sitePreferencesManualAdd').click(); } }); $('#sitePreferencesManualAdd').click(function(e) { - e.preventDefault(); - let value = $('#manualUrl').val(); + const manualUrl = document.querySelector('#manualUrl'); + if (!manualUrl) { + return; + } + + // Show error for invalid input + if (!manualUrl.validity.valid) { + options.createWarning(manualUrl, tr('optionsErrorInvalidURL')); + return; + } + + const errorMessage = tr('optionsErrorValueExists'); + let value = manualUrl.value; if (value.length > 10 && value.length <= 2000) { + // Fills the last / char if needed. This ensures the compatibility with Match Patterns + if (slashNeededForUrl(value)) { + value += '/'; + } + + // Check if the URL is already in the list + if (options.settings['sitePreferences'].some(s => s.url === value)) { + options.createWarning(manualUrl, errorMessage); + return; + } + if (options.settings['sitePreferences'] === undefined) { options.settings['sitePreferences'] = []; } @@ -360,11 +402,6 @@ options.initSitePreferences = function() { const trClone = $('#tab-site-preferences table tr.clone:first').clone(true); trClone.removeClass('clone'); - // Fills the last / char if needed. This ensures the compatibility with Match Patterns - if (slashNeededForUrl(value)) { - value += '/'; - } - const tr = trClone.clone(true); tr.data('url', value); tr.attr('id', 'tr-scf' + newValue); @@ -375,8 +412,7 @@ options.initSitePreferences = function() { options.settings['sitePreferences'].push({ url: value, ignore: IGNORE_NOTHING, usernameOnly: false }); options.saveSettings(); - - $('#manualUrl').val(''); + manualUrl.value = ''; } }); @@ -426,10 +462,44 @@ options.initSitePreferences = function() { }; options.initAbout = function() { - $('#tab-about em.versionCIP').text(browser.runtime.getManifest().version); + const version = browser.runtime.getManifest().version; + $('#tab-about em.versionCIP').text(version); + $('#tab-about span.kpxcbrVersion').text(version); + $('#tab-about span.kpxcbrOS').text(navigator.platform); + $('#tab-about span.kpxcbrBrowser').text(getBrowserId()); // Hides keyboard shortcut configure button if Firefox version is < 60 (API is not compatible) if (isFirefox() && Number(navigator.userAgent.substr(navigator.userAgent.lastIndexOf('/') + 1, 2)) < 60) { $('#chrome-only').remove(); } }; + +options.createWarning = function(elem, text) { + const banner = document.createElement('div'); + banner.classList.add('alert', 'alert-dismissible', 'alert-danger', 'fade', 'in'); + banner.style.position = 'absolute'; + banner.style.left = String(elem.offsetLeft) + 'px'; + banner.style.top = String(elem.offsetTop + elem.offsetHeight) + 'px'; + banner.style.padding = '0px'; + banner.style.width = '300px'; + banner.textContent = text; + elem.parentElement.append(banner); + + // Destroy the warning after five seconds + setTimeout(() => { + elem.parentElement.removeChild(banner); + }, 5000); +}; + +const getBrowserId = function() { + if (navigator.userAgent.indexOf('Firefox') > -1) { + return 'Mozilla Firefox ' + navigator.userAgent.substr(navigator.userAgent.lastIndexOf('/') + 1); + } else if (navigator.userAgent.indexOf('Chrome') > -1) { + let startPos = navigator.userAgent.indexOf('Chrome'); + startPos = navigator.userAgent.indexOf('/', startPos) + 1; + const version = navigator.userAgent.substring(startPos, navigator.userAgent.indexOf('Safari')); + return 'Chrome/Chromium ' + version; + } + + return 'Other/Unknown'; +}; diff --git a/keepassxc-browser/options/shortcuts.html b/keepassxc-browser/options/shortcuts.html index c516fca..69c2638 100644 --- a/keepassxc-browser/options/shortcuts.html +++ b/keepassxc-browser/options/shortcuts.html @@ -24,23 +24,29 @@
-
- +
+
-
- +
+
-
- +
+
+
+
+ + + +
diff --git a/keepassxc-browser/options/shortcuts.js b/keepassxc-browser/options/shortcuts.js index 3ac8519..95bd0c9 100644 --- a/keepassxc-browser/options/shortcuts.js +++ b/keepassxc-browser/options/shortcuts.js @@ -11,14 +11,14 @@ document.querySelectorAll('input').forEach((b) => { const saveButtons = document.querySelectorAll('.btn-primary'); for (const b of saveButtons) { b.addEventListener('click', (e) => { - updateShortcut(b.parentElement.children[1].getAttribute('id')) + updateShortcut(b.parentElement.children[1].getAttribute('id')); }); } const resetButtons = document.querySelectorAll('.btn-danger'); for (const b of resetButtons) { b.addEventListener('click', (e) => { - resetShortcut(b.parentElement.children[1].getAttribute('id')) + resetShortcut(b.parentElement.children[1].getAttribute('id')); }); } @@ -31,7 +31,7 @@ async function handleKeyDown(e) { tempArray.push(key); keyArray.push(key); } -}; +} async function handleKeyUp(e) { if (!e.repeat) { @@ -55,7 +55,7 @@ async function handleKeyUp(e) { e.currentTarget.value = text; } } -}; +} async function updateKeys() { const commands = await browser.commands.getAll(); @@ -65,7 +65,7 @@ async function updateKeys() { elem.value = c.shortcut; } } -}; +} async function updateShortcut(shortcut) { try { @@ -78,13 +78,13 @@ async function updateShortcut(shortcut) { console.log('Cannot change shortcut: ' + e); createBanner('danger', shortcut); } -}; +} async function resetShortcut(shortcut) { await browser.commands.reset(shortcut); createBanner('info', shortcut); updateKeys(); -}; +} // Ctrl behaves differently on different OS's. macOS needs to return MacCtrl instead of Ctrl (which will be handled as Command) function handleControl() { @@ -112,6 +112,6 @@ function createBanner(type, shortcut) { setTimeout(() => { document.body.removeChild(banner); }, 5000); -}; +} document.addEventListener('DOMContentLoaded', updateKeys); diff --git a/keepassxc-browser/translate.js b/keepassxc-browser/translate.js index e8713dc..0d7062e 100644 --- a/keepassxc-browser/translate.js +++ b/keepassxc-browser/translate.js @@ -2,14 +2,33 @@ const items = document.querySelectorAll('[data-i18n]'); for (const item of items) { - const key = item.getAttribute('data-i18n'); + let key = item.getAttribute('data-i18n'); if (key) { + let attr = ''; + [ attr, key] = trAttribute(key); + const placeholder = item.getAttribute('i18n-placeholder'); const translation = placeholder ? browser.i18n.getMessage(key, placeholder) : browser.i18n.getMessage(key); - if (item.hasAttribute('href')) { + if (attr) { + item.setAttribute(attr, translation); + } else if (item.hasAttribute('href')) { item.text = translation; } else { item.innerHTML = translation; } + + // Remove the translation attribute from the HTML element + item.removeAttribute('data-i18n'); } } + +// Check if a custom attribute is being translated using [attribute]key +function trAttribute(key) { + let attr = /\[(\w+)\]/.exec(key); + if (attr) { + attr = attr[1]; + key = key.slice(key.indexOf(']') + 1); + } + + return [ attr, key ]; +};
-