Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/marius-wieschollek/passwords-webextension.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarius David Wieschollek <passwords.public@mdns.eu>2021-04-11 11:25:42 +0300
committerMarius David Wieschollek <passwords.public@mdns.eu>2021-04-11 11:25:42 +0300
commit38c0e18a18a29b07dd7e14b0955a9640e8175f8d (patch)
tree9b68c6812701fbee437a39faff49301d40e3f523
parente11c309c22118850576016a7849882a903094734 (diff)
parent6ebfb621d5d25b3d679f1a1a244307cf9e0c0a97 (diff)
Signed-off-by: Marius David Wieschollek <passwords.public@mdns.eu>
-rw-r--r--src/js/Controller/Client/FillPassword.js23
-rw-r--r--src/js/Controller/Password/Fill.js2
-rw-r--r--src/js/Controller/Password/Update.js124
-rw-r--r--src/js/Manager/AutofillManager.js29
-rw-r--r--src/js/Manager/ControllerManager.js7
-rw-r--r--src/js/Manager/MiningManager.js106
-rw-r--r--src/platform/generic/_locales/de/messages.json66
-rw-r--r--src/platform/generic/_locales/en/messages.json66
-rw-r--r--src/vue/App/Popup.vue7
-rw-r--r--src/vue/Components/Collected/MinedProperty.vue149
-rw-r--r--src/vue/Components/Collected/MiningItem.vue54
-rw-r--r--src/vue/Components/Form/InputField.vue18
-rw-r--r--src/vue/Components/Form/SliderField.vue9
-rw-r--r--src/vue/Components/Icon.vue1
-rw-r--r--src/vue/Components/List/Item/Menu/PasswordMenu.vue3
-rw-r--r--src/vue/Components/List/Item/Password.vue24
-rw-r--r--src/vue/Components/Password/CustomProperty.vue338
-rw-r--r--src/vue/Components/Password/Mining.vue210
-rw-r--r--src/vue/Components/Password/Property.vue301
-rw-r--r--src/vue/Components/Password/View.vue315
-rw-r--r--src/vue/Components/Popup/Collected.vue6
21 files changed, 1636 insertions, 222 deletions
diff --git a/src/js/Controller/Client/FillPassword.js b/src/js/Controller/Client/FillPassword.js
index c227b64..3aa416e 100644
--- a/src/js/Controller/Client/FillPassword.js
+++ b/src/js/Controller/Client/FillPassword.js
@@ -11,7 +11,7 @@ export default class FillPassword extends AbstractController {
*/
async execute(message, reply) {
try {
- let result = this._fillPassword(message.getPayload().user, message.getPayload().password, message.getPayload().submit);
+ let result = this._fillPassword(message.getPayload().user, message.getPayload().password, message.getPayload().submit, message.getPayload().formFields);
if(result) reply.setPayload(true);
} catch(e) {
@@ -24,12 +24,15 @@ export default class FillPassword extends AbstractController {
* @param {String} user
* @param {String} password
* @param {Boolean} trySubmit
+ * @param {Array} formFields
* @returns {Boolean}
*/
- _fillPassword(user, password, trySubmit) {
+ _fillPassword(user, password, trySubmit, formFields) {
let forms = new FormService().getLoginFields();
if(forms.length === 0) return false;
+ this._fillCustomForms(formFields);
+
for(let i = 0; i < forms.length; i++) {
let form = forms[i];
if(form.user) this._insertTextIntoField(form.user, user);
@@ -46,6 +49,22 @@ export default class FillPassword extends AbstractController {
return true;
}
+ /**
+ *
+ * @param {Array} formFields
+ */
+ _fillCustomForms(formFields) {
+ formFields.forEach((field) => {
+ var element = document.getElementById(field.id);
+ if(element !== null && element !== undefined) {
+ if(!element.readOnly && !element.disabled && !element.hidden) {
+ this._insertTextIntoField(element, field.value);
+ }
+ }
+ })
+
+ }
+
/**
*
* @param {Element} field
diff --git a/src/js/Controller/Password/Fill.js b/src/js/Controller/Password/Fill.js
index 13fc853..176931a 100644
--- a/src/js/Controller/Password/Fill.js
+++ b/src/js/Controller/Password/Fill.js
@@ -6,6 +6,7 @@ import SettingsService from '@js/Services/SettingsService';
import ErrorManager from '@js/Manager/ErrorManager';
import PasswordStatisticsService from "@js/Services/PasswordStatisticsService";
import Message from "@js/Models/Message/Message";
+import AutofillManager from "@js/Manager/AutofillManager";
export default class Fill extends AbstractController {
@@ -37,6 +38,7 @@ export default class Fill extends AbstractController {
payload : {
user : password.getUserName(),
password: password.getPassword(),
+ formFields: AutofillManager.getCustomFormFields(password),
submit : await SettingsService.getValue('paste.form.submit')
}
}
diff --git a/src/js/Controller/Password/Update.js b/src/js/Controller/Password/Update.js
new file mode 100644
index 0000000..cdbcef5
--- /dev/null
+++ b/src/js/Controller/Password/Update.js
@@ -0,0 +1,124 @@
+import AbstractController from '@js/Controller/AbstractController';
+import SearchIndex from '@js/Search/Index/SearchIndex';
+import ApiRepository from "@js/Repositories/ApiRepository";
+import ToastService from "@js/Services/ToastService";
+import ErrorManager from "@js/Manager/ErrorManager";
+import HiddenFolderHelper from "@js/Helper/HiddenFolderHelper";
+import SearchQuery from '@js/Search/Query/SearchQuery';
+
+export default class Update extends AbstractController {
+
+ async execute(message, reply) {
+ let {data} = message.getPayload(),
+ query = new SearchQuery(),
+ password = /** @type {EnhancedPassword} **/ query
+ .where(query.field('id').equals(data.id))
+ .hidden(true|false)
+ .execute()[0],
+ api = /** @type {PasswordsClient} **/ await ApiRepository.findById(password.getServer().getId());
+
+ if(password !== null && !password.isTrashed()) {
+ password
+ .setFavorite(this._setProperty('favorite', data, password))
+ .setLabel(this._setProperty('label', data, password))
+ .setUserName(this._setProperty('username', data, password))
+ .setPassword(this._setProperty('password', data, password))
+ .setUrl(this._setProperty('url', data, password))
+ .setEdited(this._setEdited(data, password))
+ .setCustomFields(this._setProperty('customFields', data, password))
+ .setNotes(this._setProperty('notes', data, password))
+ .setFolder(await this._setFolder(api, data, password))
+ .setHidden(this._setProperty('hidden', data, password));
+
+ await this._updatePassword(api, password);
+ reply.setPayload({success: true, data: data});
+ } else {
+ reply.setPayload({success: false});
+ this._returnError()
+ }
+ }
+
+ /**
+ *
+ * @param {String} property
+ * @param {JSON} data
+ * @param {EnhancedPassword} password
+ * @returns {*}
+ * @private
+ */
+ _setProperty(property, data, password) {
+ if(data.hasOwnProperty(property)) {
+ return data[property];
+ } else {
+ return password.getProperty(property);
+ }
+ }
+
+ /**
+ *
+ * @param {JSON} data
+ * @param {EnhancedPassword} password
+ * @returns {Date}
+ * @private
+ */
+ _setEdited(data, password) {
+ var updateEdited = false;
+ for(var item in data) {
+ if(item !== 'id' && item !== 'favorite') {
+ updateEdited = true;
+ }
+ }
+ return updateEdited ? new Date():password.getEdited();
+ }
+
+ /**
+ *
+ * @param {String} property
+ * @param {JSON} data
+ * @param {EnhancedPassword} password
+ * @returns {String}
+ * @private
+ */
+ async _setFolder(api, data, password) {
+ if(!data.hasOwnProperty('hidden')) {
+ return password.getFolder();
+ }
+
+ let helper = new HiddenFolderHelper();
+ var hiddenFolder = await helper.getHiddenFolderId(api);
+ if(data.hidden && password.getFolder() !== hiddenFolder) {
+ return hiddenFolder;
+ } else if(!data.hidden && password.getFolder() === hiddenFolder) {
+ return "00000000-0000-0000-0000-000000000000";
+ }
+ return password.getFolder();
+ }
+
+ /**
+ *
+ * @param {PasswordsClient} api
+ * @param {EnhancedPassword} password
+ * @private
+ */
+ async _updatePassword(api, password) {
+ let repository = /** @type {PasswordRepository} **/ api.getInstance('repository.password');
+ await repository.update(password);
+ password = await repository.findById(password.getId());
+
+ SearchIndex.removeItem(password);
+ SearchIndex.addItem(password, true);
+ ToastService.success('ToastPasswordUpdated')
+ .catch(ErrorManager.catch);
+ }
+
+ /**
+ *
+ * @private
+ */
+ _returnError() {
+ ToastService
+ .error('ToastPasswordUpdateFailed')
+ .catch(ErrorManager.catch);
+ }
+
+} \ No newline at end of file
diff --git a/src/js/Manager/AutofillManager.js b/src/js/Manager/AutofillManager.js
index 16f122c..f278e8c 100644
--- a/src/js/Manager/AutofillManager.js
+++ b/src/js/Manager/AutofillManager.js
@@ -91,11 +91,34 @@ export default new class AutofillManager {
tab : TabManager.currentTabId,
silent : true,
payload : {
- user : password.getUserName(),
- password: password.getPassword(),
- submit : false
+ user : password.getUserName(),
+ password : password.getPassword(),
+ formFields: this.getCustomFormFields(password),
+ submit : false
}
}
).catch(ErrorManager.catchEvt);
}
+
+ /**
+ *
+ * @param {Password} password
+ * @returns {Array}
+ * @private
+ */
+ getCustomFormFields(password) {
+ var formFields = [];
+ var customFields = password.getCustomFields();
+ customFields._elements.forEach((e) => {
+ if(e.getType() === 'data' && e.getLabel().startsWith('ext:field/')) {
+ formFields.push(
+ {
+ id : e.getLabel().replace('ext:field/', ''),
+ value: e.getValue()
+ }
+ )
+ }
+ })
+ return formFields;
+ }
};
diff --git a/src/js/Manager/ControllerManager.js b/src/js/Manager/ControllerManager.js
index 51e31fa..c8e0691 100644
--- a/src/js/Manager/ControllerManager.js
+++ b/src/js/Manager/ControllerManager.js
@@ -68,6 +68,13 @@ class ControllerManager {
}
);
MessageService.listen(
+ 'password.update',
+ async (message, reply) => {
+ let module = await import(/* webpackChunkName: "PasswordUpdate" */ '@js/Controller/Password/Update');
+ await this._executeController(module, message, reply);
+ }
+ );
+ MessageService.listen(
'folder.list',
async (message, reply) => {
let module = await import(/* webpackChunkName: "FolderList" */ '@js/Controller/Folder/List');
diff --git a/src/js/Manager/MiningManager.js b/src/js/Manager/MiningManager.js
index ccb2eaf..59f9691 100644
--- a/src/js/Manager/MiningManager.js
+++ b/src/js/Manager/MiningManager.js
@@ -85,7 +85,11 @@ class MiningManager {
.setTaskField('username', data.user.value)
.setTaskField('password', data.password.value)
.setTaskField('url', data.url)
+ .setTaskField('notes', '')
.setTaskField('hidden', hidden)
+ .setTaskField('created', '')
+ .setTaskField('edited', '')
+ .setTaskField('customFields', [])
.setTaskManual(data.manual)
.setTaskNew(true);
@@ -96,6 +100,10 @@ class MiningManager {
.setTaskField('label', basePassword.getLabel())
.setTaskField('url', basePassword.getUrl())
.setTaskField('hidden', basePassword.getHidden())
+ .setTaskField('notes', basePassword.getNotes())
+ .setTaskField('created', basePassword.getCreated())
+ .setTaskField('edited', basePassword.getEdited())
+ .setTaskField('customFields', basePassword.getCustomFields())
.setTaskNew(false);
}
}
@@ -151,6 +159,7 @@ class MiningManager {
password.setFolder(await helper.getHiddenFolderId(api));
}
+ password = this._enforcePasswordPropertyLengths(password);
await api.getPasswordRepository().create(password);
SearchIndex.addItem(password);
@@ -167,6 +176,7 @@ class MiningManager {
query = new SearchQuery(),
password = /** @type {EnhancedPassword} **/ query
.where(query.field('id').equals(task.getResultField('id')))
+ .hidden(true|false)
.execute()[0];
password
@@ -175,15 +185,19 @@ class MiningManager {
.setPassword(task.getResultField('password'))
.setUrl(task.getResultField('url'))
.setEdited(new Date())
+ .setCustomFields(task.getResultField('customFields'))
+ .setNotes(task.getResultField('notes'))
+ .setFolder(await this._setFolder(api, task, password))
.setHidden(task.getResultField('hidden'));
- this._enforcePasswordPropertyLengths(password);
+ password = this._enforcePasswordPropertyLengths(password);
if(password.isHidden()) {
let helper = new HiddenFolderHelper();
password.setFolder(await helper.getHiddenFolderId(api));
}
await api.getPasswordRepository().update(password);
+ password = await api.getPasswordRepository().findById(password.getId());
SearchIndex.removeItem(password);
SearchIndex.addItem(password);
@@ -192,6 +206,29 @@ class MiningManager {
}
/**
+ *
+ * @param {String} property
+ * @param {MiningItem} task
+ * @param {EnhancedPassword} password
+ * @returns {EnhancedPassword}
+ * @private
+ */
+ async _setFolder(api, task, password) {
+ if(task.getResultField('hidden') === password.getHidden()) {
+ return password.getFolder();
+ }
+
+ let helper = new HiddenFolderHelper();
+ var hiddenFolder = await helper.getHiddenFolderId(api);
+ if(task.getResultField('hidden') && password.getFolder() !== hiddenFolder) {
+ return hiddenFolder;
+ } else if(!task.getResultField('hidden') && password.getFolder() === hiddenFolder) {
+ return "00000000-0000-0000-0000-000000000000";
+ }
+ return password.getFolder();
+ }
+
+ /**
* @param {Object} data
* @return {Boolean}
*/
@@ -293,6 +330,7 @@ class MiningManager {
/**
*
* @param {Password} password
+ * @returns {Password}
* @private
*/
_enforcePasswordPropertyLengths(password) {
@@ -308,6 +346,72 @@ class MiningManager {
if(password.getUrl().length > 2048) {
password.setUrl(password.getUrl().substr(0, 2048));
}
+ if(password.getNotes().length > 4096) {
+ password.setNotes(password.getNotes().substr(0, 4096));
+ }
+ return this._enforcePasswordCustomPropertyLengths(password);
+ }
+
+ /**
+ *
+ * @param {Password} password
+ * @returns {Password}
+ * @private
+ */
+ _enforcePasswordCustomPropertyLengths(password) {
+ let customFields = password.getCustomFields();
+ if(Array.isArray(customFields._elements)) {
+ password.setCustomFields(this._enforcePasswordCustomPropertyLengthsInObject(customFields));
+ } else {
+ password.setCustomFields(this._enforcePasswordCustomPropertyLengthsInArray(customFields));
+ }
+ return password;
+ }
+
+ /**
+ *
+ * @param {Array} fields
+ * @returns {Array}
+ * @private
+ */
+ _enforcePasswordCustomPropertyLengthsInArray(fields) {
+ for(var i = 0; i < fields.length; i++) {
+ if((fields[i].label === "" && fields[i].value === "" && fields[i].type !== "data" && fields[i].type !== "file")
+ || fields[i].label === "ext:field/" && fields[i].type === 'data') {
+
+ fields.splice(i, 1);
+ i--;
+ }
+ }
+ if(fields.length > 0) {
+ while(JSON.stringify(fields).length > 8192) {
+ fields.pop()
+ }
+ }
+ return fields;
+ }
+
+ /**
+ *
+ * @param {Object} fields
+ * @returns {Object}
+ * @private
+ */
+ _enforcePasswordCustomPropertyLengthsInObject(fields) {
+ for(var i = 0; i < fields.length; i++) {
+ var field = fields.get(i);
+ if((field.getLabel() === "" && field.getValue() === "" && field.getType() !== "data" && field.getType() !== "file")
+ || field.getLabel() === "ext:field/" && field.getType() === 'data') {
+ fields._elements.splice(i, 1);
+ i--;
+ }
+ }
+ if(fields.length > 0) {
+ while(JSON.stringify(fields).length > 8192) {
+ fields._elements.splice(fields.length -1, 1);
+ }
+ }
+ return fields;
}
}
diff --git a/src/platform/generic/_locales/de/messages.json b/src/platform/generic/_locales/de/messages.json
index fdb9d81..d814e24 100644
--- a/src/platform/generic/_locales/de/messages.json
+++ b/src/platform/generic/_locales/de/messages.json
@@ -243,6 +243,10 @@
"message" : "In den Papierkorb verschieben",
"description": "Title of the menu option to move a password to the trash"
},
+ "PasswordItemViewEdit" : {
+ "message" : "Passwort ansehen/bearbeiten",
+ "description": "Title of the menu option to view and edit a password"
+ },
"ValidationLabel" : {
"message" : "Titel",
"description": "Name of the account label field in account validation message. This value is used when the user attempts to save an account with an invalid label"
@@ -455,6 +459,26 @@
"message" : "Ordner",
"description": "Label for the folder count in the server info in the browse tab"
},
+ "LabelFolder" : {
+ "message" : "Ordner",
+ "description": "Label for the folder of this password."
+ },
+ "LabelCreated" : {
+ "message" : "Erstellt",
+ "description": "Label for the creation time of this password."
+ },
+ "LabelEdited" : {
+ "message" : "Geändert",
+ "description": "Label for the last edit time of this password."
+ },
+ "LabelNotes" : {
+ "message" : "Notitzen",
+ "description": "Label for the notes of this password."
+ },
+ "LabelCustomFields" : {
+ "message" : "Eigene Felder",
+ "description": "Label for the custom fields of this password"
+ },
"LabelTags" : {
"message" : "Tags",
"description": "Label for the tag count in the server info in the browse tab"
@@ -641,6 +665,14 @@
}
}
},
+ "ToastPasswordUpdated" : {
+ "message" : "Passwort aktualisiert",
+ "description": "Text of the toast notification when password was updated"
+ },
+ "ToastPasswordUpdateFailed" : {
+ "message" : "Passwort aktualisierung fehlgeschlagen",
+ "description": "Text of the toast notification when password update failed"
+ },
"PasswordPastedSuccess" : {
"message" : "$LABEL$ eingefügt",
"description" : "Text of the toast notification when a password entry was pasted successfully into the current tab",
@@ -1314,5 +1346,39 @@
"InputSliderOff" : {
"message" : "ausgeschaltet",
"description": "Tooltip of any slider input element that is currently in the inactive/disabled state"
+ },
+ "PasswordCustomFieldsTypeText" : {
+ "message" : "Text",
+ "description": "Label of the password custom setting type 'text'."
+ },
+ "PasswordCustomFieldsTypeSecret" : {
+ "message" : "Geheimnis",
+ "description": "Label of the password custom setting type 'secret'."
+ },
+ "PasswordCustomFieldsTypeEmail" : {
+ "message" : "Email",
+ "description": "Label of the password custom setting type 'email'."
+ },
+ "PasswordCustomFieldsTypeUrl" : {
+ "message" : "Link",
+ "description": "Label of the password custom setting type 'url'."
+ },
+ "PasswordCustomFieldsTypeFormField" : {
+ "message" : "Form Feld",
+ "description": "Label of the password custom setting form field to insert custom values to a web page."
+ },
+ "PasswordEditInvalidValue" : {
+ "message" : "Ungültiger Wert",
+ "description": "Label of the password edit 'invalid value' message."
+ },
+ "PasswordEditMaxAllowedCharacter" : {
+ "message" : "Maximal $CHARACTERS$ Zeichen erlaubt",
+ "description": "Label of the password edit max allowed character message.",
+ "placeholders": {
+ "characters": {
+ "content": "$1",
+ "example": "One of 1, 48, 320, 370, 2048 or 8192"
+ }
+ }
}
}
diff --git a/src/platform/generic/_locales/en/messages.json b/src/platform/generic/_locales/en/messages.json
index 619b7a2..204b9e8 100644
--- a/src/platform/generic/_locales/en/messages.json
+++ b/src/platform/generic/_locales/en/messages.json
@@ -257,6 +257,10 @@
"message" : "Move to trash",
"description": "Title of the menu option to move a password to the trash"
},
+ "PasswordItemViewEdit" : {
+ "message" : "View/edit password",
+ "description": "Title of the menu option to view and edit a password"
+ },
"ValidationLabel" : {
"message" : "label",
"description": "Name of the account label field in account validation message. This value is used when the user attempts to save an account with an invalid label"
@@ -469,6 +473,26 @@
"message" : "Folders",
"description": "Label for the folder count in the server info in the browse tab"
},
+ "LabelFolder" : {
+ "message" : "Folder",
+ "description": "Label for the folder of this password."
+ },
+ "LabelCreated" : {
+ "message" : "Created",
+ "description": "Label for the creation time of this password."
+ },
+ "LabelEdited" : {
+ "message" : "Modified",
+ "description": "Label for the last edit time of this password."
+ },
+ "LabelNotes" : {
+ "message" : "Notes",
+ "description": "Label for the notes of this password."
+ },
+ "LabelCustomFields" : {
+ "message" : "Custom Fields",
+ "description": "Label for the custom fields of this password"
+ },
"LabelTags" : {
"message" : "Tags",
"description": "Label for the tag count in the server info in the browse tab"
@@ -665,6 +689,14 @@
}
}
},
+ "ToastPasswordUpdated" : {
+ "message" : "Password updated",
+ "description": "Text of the toast notification when password was updated"
+ },
+ "ToastPasswordUpdateFailed" : {
+ "message" : "Password update failed",
+ "description": "Text of the toast notification when password update failed"
+ },
"PasswordPastedError" : {
"message" : "Could not paste $LABEL$",
"description" : "Text of the toast notification when a password could not be pasted into the current tab",
@@ -1328,5 +1360,39 @@
"InputSliderOff" : {
"message" : "Currently off",
"description": "Tooltip of any slider input element that is currently in the inactive/disabled state"
+ },
+ "PasswordCustomFieldsTypeText" : {
+ "message" : "Text",
+ "description": "Label of the password custom setting type 'text'."
+ },
+ "PasswordCustomFieldsTypeSecret" : {
+ "message" : "Secret",
+ "description": "Label of the password custom setting type 'secret'."
+ },
+ "PasswordCustomFieldsTypeEmail" : {
+ "message" : "Email",
+ "description": "Label of the password custom setting type 'email'."
+ },
+ "PasswordCustomFieldsTypeUrl" : {
+ "message" : "Link",
+ "description": "Label of the password custom setting type 'url'."
+ },
+ "PasswordCustomFieldsTypeFormField" : {
+ "message" : "Form field",
+ "description": "Label of the password custom setting form field to insert custom values to a web page."
+ },
+ "PasswordEditInvalidValue" : {
+ "message" : "Invalid value",
+ "description": "Label of the password edit 'invalid value' message."
+ },
+ "PasswordEditMaxAllowedCharacter" : {
+ "message" : "Only $CHARACTERS$ characters allowed",
+ "description": "Label of the password edit max allowed character message.",
+ "placeholders": {
+ "characters": {
+ "content": "$1",
+ "example": "One of 1, 48, 320, 370, 2048 or 8192"
+ }
+ }
}
}
diff --git a/src/vue/App/Popup.vue b/src/vue/App/Popup.vue
index 7030c45..14bcee6 100644
--- a/src/vue/App/Popup.vue
+++ b/src/vue/App/Popup.vue
@@ -112,8 +112,11 @@
.send({type: 'popup.status.set', payload: {tab}});
},
searchEvent($event) {
- this.search.query = $event;
- this.$refs.tabs.setActive('search');
+ var pwdViews = document.getElementsByClassName("password-view")
+ if(pwdViews.length === 0) {
+ this.search.query = $event;
+ this.$refs.tabs.setActive('search');
+ }
}
}
};
diff --git a/src/vue/Components/Collected/MinedProperty.vue b/src/vue/Components/Collected/MinedProperty.vue
deleted file mode 100644
index 78541e5..0000000
--- a/src/vue/Components/Collected/MinedProperty.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-<template>
- <div :class="classList">
- <label :for="id" @dblclick="edit()" :title="title">{{ label }}</label>
- <div @dblclick="edit()" v-if="!editing" :title="title">{{ text }}</div>
- <input-field :id="id" v-model="value" @keypress="checkEnter($event)" title="TitleEnterToExit" v-else-if="type === 'text'"/>
- <slider-field :id="id" v-model="value" v-else/>
- </div>
-</template>
-
-<script>
- import LocalisationService from '@js/Services/LocalisationService';
- import MiningItem from '@js/Models/Queue/MiningItem';
- import MessageService from '@js/Services/MessageService';
- import InputField from '@vue/Components/Form/InputField';
- import SliderField from "@vue/Components/Form/SliderField";
-
- export default {
- components: {SliderField, InputField},
- props : {
- item : {
- type: MiningItem
- },
- field: {
- type: String
- }
- },
-
- data() {
- return {
- editing: this.field === 'hidden',
- value : this.item.getResultField(this.field)
- };
- },
-
- computed: {
- label() {
- if(['label', 'url', 'username', 'password', 'hidden'].indexOf(this.field) !== -1) {
- return LocalisationService.translate(`Label${this.field.capitalize()}`);
- }
-
- return this.field;
- },
- text() {
- return this.field === 'password' ? '':this.value;
- },
- classList() {
- return `mining-property field-${this.field}`;
- },
- id() {
- return `property-${this.field}`;
- },
- type() {
- return this.field === 'hidden' ? 'checkbox':'text';
- },
- title() {
- if(this.field === 'hidden') return '';
- return this.editing ? LocalisationService.translate('TitleEnterToExit'):LocalisationService.translate('TitleClickToEdit');
- }
- },
-
- methods: {
- edit() {
- this.editing = true;
- },
- checkEnter($event) {
- if($event.key === 'Enter') {
- $event.preventDefault();
- this.editing = false;
- this.item.setResultField(this.field, this.value);
-
- MessageService
- .send({type: 'popup.mining.update', payload: this.item});
- }
- }
- },
- watch : {
- value(value) {
- this.item.setResultField(this.field, value);
- }
- }
-
- };
-</script>
-
-<style lang="scss">
-.mining-property {
- padding : .5rem;
-
- label {
- cursor : pointer;
- display : block;
- font-weight : bold;
- color : var(--element-active-fg-color)
- }
-
- div {
- cursor : pointer;
- padding : .25rem;
- box-sizing : border-box;
- border-radius : 3px;
- border : none;
- line-height : 2rem;
- height : 2.5rem;
- white-space : nowrap;
- text-overflow : ellipsis;
- overflow : hidden;
- box-shadow : 0 0 0 1px transparent;
- transition : box-shadow .15s ease-in-out;
- color : var(--element-fg-color);
-
- &:hover {
- box-shadow : 0 0 0 1px var(--element-hover-bg-color);
- }
- }
-
- input {
- width : 100%;
- padding : .25rem;
- box-sizing : border-box;
- box-shadow : 0 0 0 1px var(--element-active-fg-color);
- border-radius : 3px;
- border : none;
- line-height : 2rem;
- background-color : var(--element-bg-color);
- color : var(--element-fg-color);
- }
-
- &.field-password {
- div {
- font-family : "Font Awesome 5 Free", sans-serif;
- font-size : .5rem;
- letter-spacing : .25rem;
- font-weight : bold;
- }
- }
-
- &.field-hidden {
- display : flex;
-
- label {
- flex-grow : 1;
- }
-
- .input-slider {
- margin-top : 2px;
- }
- }
-}
-</style> \ No newline at end of file
diff --git a/src/vue/Components/Collected/MiningItem.vue b/src/vue/Components/Collected/MiningItem.vue
deleted file mode 100644
index b43a7a8..0000000
--- a/src/vue/Components/Collected/MiningItem.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
- <div class="mining-item">
- <translate say="MiningItemIsNew" class="create-info" v-if="item.isNew()">
- <icon slot="before" icon="info-circle" font="solid"/>
- </translate>
- <translate say="MiningItemIsUpdate" class="create-info" :variables="updateVariables" v-else>
- <icon slot="before" icon="info-circle" font="solid"/>
- </translate>
- <mined-property :field="field" :item="item" v-for="field in fields" :key="field"/>
- </div>
-</template>
-
-<script>
- import MiningItem from '@js/Models/Queue/MiningItem';
- import MinedProperty from '@vue/Components/Collected/MinedProperty';
- import Translate from '@vue/Components/Translate';
- import Icon from '@vue/Components/Icon';
-
- export default {
- components: {Icon, Translate, MinedProperty},
- props : {
- item: {
- type: MiningItem
- }
- },
- computed : {
- updateVariables() {
- return [
- this.item.getTask().fields.label
- ];
- },
- fields() {
- let resultFields = this.item.listResultFields(),
- fields = [];
-
- for(let resultField of resultFields) {
- if(resultField !== 'id') fields.push(resultField);
- }
-
- return fields;
- }
- }
- };
-</script>
-
-<style lang="scss">
-.mining-item {
- .create-info {
- display : block;
- padding : 1rem .5rem .25rem .5rem;
- color : var(--element-active-fg-color);
- }
-}
-</style> \ No newline at end of file
diff --git a/src/vue/Components/Form/InputField.vue b/src/vue/Components/Form/InputField.vue
index 65b3e94..e902f54 100644
--- a/src/vue/Components/Form/InputField.vue
+++ b/src/vue/Components/Form/InputField.vue
@@ -1,9 +1,21 @@
<template>
- <input :type="type"
+ <textarea v-if="type === 'textarea'"
+ :value="value"
+ :placeholder="getPlaceholder"
+ :title="getTitle"
+ :readonly="readonly"
+ filled
+ auto-grow
+ v-on="listeners"
+ @input="handleInput"
+ @change="handleChange"/>
+ <input v-else
+ :type="type"
:value="value"
:checked="isChecked"
:placeholder="getPlaceholder"
:title="getTitle"
+ :readonly="readonly"
v-on="listeners"
@input="handleInput"
@change="handleChange"/>
@@ -34,6 +46,10 @@
title : {
type : String,
default: ''
+ },
+ readonly : {
+ type : Boolean,
+ default: false
}
},
diff --git a/src/vue/Components/Form/SliderField.vue b/src/vue/Components/Form/SliderField.vue
index 729c113..e1acbf4 100644
--- a/src/vue/Components/Form/SliderField.vue
+++ b/src/vue/Components/Form/SliderField.vue
@@ -6,6 +6,7 @@
ref="checkbox"
:id="id"
:name="name"
+ :readonly="readonly"
v-model="model"
v-on="listeners" />
</span>
@@ -31,6 +32,10 @@
name : {
type : String,
default: undefined
+ },
+ readonly: {
+ type : Boolean,
+ default: false
}
},
@@ -66,7 +71,9 @@
methods: {
toggleSwitch() {
- this.model = !this.model;
+ if(this.readonly === false) {
+ this.model = !this.model;
+ }
}
},
diff --git a/src/vue/Components/Icon.vue b/src/vue/Components/Icon.vue
index cdbc5b4..b8f6a2f 100644
--- a/src/vue/Components/Icon.vue
+++ b/src/vue/Components/Icon.vue
@@ -50,7 +50,6 @@
let icon = this.hover && this.hoverIcon !== null ? this.hoverIcon:this.icon,
font = this.hover && this.hoverFont !== null ? this.hoverFont:this.font,
style = font === 'solid' ? 'fas':'far';
-
if(this.spin) style += ' fa-spin';
return `icon icon-${icon} font-${font} ${style} fa-${icon}`;
diff --git a/src/vue/Components/List/Item/Menu/PasswordMenu.vue b/src/vue/Components/List/Item/Menu/PasswordMenu.vue
index 93b0ef8..f343694 100644
--- a/src/vue/Components/List/Item/Menu/PasswordMenu.vue
+++ b/src/vue/Components/List/Item/Menu/PasswordMenu.vue
@@ -4,6 +4,9 @@
<icon icon="globe-europe" font="solid" slot="before"/>
<icon class="option" icon="clipboard" slot="after" @click.stop="$emit('copy', 'url')"/>
</translate>
+ <translate tag="div" class="menu-item" say="PasswordItemViewEdit" @click="$emit('toggleEntry')">
+ <icon icon="file-alt" font="solid" slot="before"/>
+ </translate>
<translate tag="div" class="menu-item" say="PasswordItemToTrash" @click="moveToTrash">
<icon icon="trash" font="solid" slot="before"/>
</translate>
diff --git a/src/vue/Components/List/Item/Password.vue b/src/vue/Components/List/Item/Password.vue
index e5c1163..0b154b8 100644
--- a/src/vue/Components/List/Item/Password.vue
+++ b/src/vue/Components/List/Item/Password.vue
@@ -1,6 +1,6 @@
<template>
<li class="item password-item">
- <div class="item-main" :class="{'has-menu':showMenu}">
+ <div class="item-main" :class="{'has-menu':(showMenu || showEntry ? true : false)}">
<div class="label" @click="sendPassword()" :title="title" v-on:transitionend="calculateOverflow">
<favicon :password="password.getId()" :size="32" v-if="favicon" />
<div ref="scrollContainer" class="scroll-container" :style="titleVars">
@@ -10,11 +10,12 @@
<div class="options">
<icon icon="user" hover-icon="clipboard" @click="copy('username')" draggable="true" @dragstart="drag($event, 'username')" />
<icon icon="key" font="solid" hover-icon="clipboard" hover-font="regular" @click="copy('password')" draggable="true" @dragstart="drag($event, 'password')" />
- <icon icon="ellipsis-h" font="solid" @click="showMenu = !showMenu" />
+ <icon icon="ellipsis-h" font="solid" @click="toggleMenu()" />
</div>
<icon :class="securityClass" icon="shield-alt" font="solid" />
</div>
- <password-menu :show="showMenu" :password="password" v-on:copy="copy($event)" v-on:delete="$emit('delete', password)" />
+ <password-menu :show="showMenu" :password="password" v-on:copy="copy($event)" v-on:delete="$emit('delete', password)" v-on:toggleEntry="toggleEntry()"/>
+ <password-view v-if="showEntry" :password="password" v-on:toggleEntry="toggleEntry()"/>
</li>
</template>
@@ -30,9 +31,10 @@
import PasswordSettingsManager from '@js/Manager/PasswordSettingsManager';
import Translate from '@vue/Components/Translate';
import PasswordMenu from '@vue/Components/List/Item/Menu/PasswordMenu';
+ import PasswordView from '@vue/Components/Password/View';
export default {
- components: {PasswordMenu, Translate, Favicon, Icon},
+ components: {PasswordMenu, Translate, Favicon, Icon, PasswordView},
props : {
password: {
type: Password
@@ -51,7 +53,8 @@
return {
active : true,
showMenu: false,
- overflow: 0
+ overflow: 0,
+ showEntry: false
};
},
@@ -140,6 +143,17 @@
drag(event, property) {
let data = this.password.getProperty(property);
event.dataTransfer.setData('text/plain', data);
+ },
+ toggleMenu() {
+ if(this.showEntry === true) {
+ this.showEntry = false;
+ } else {
+ this.showMenu = !this.showMenu;
+ }
+ },
+ toggleEntry() {
+ this.showMenu = false;
+ this.showEntry = !this.showEntry;
}
}
};
diff --git a/src/vue/Components/Password/CustomProperty.vue b/src/vue/Components/Password/CustomProperty.vue
new file mode 100644
index 0000000..1e05467
--- /dev/null
+++ b/src/vue/Components/Password/CustomProperty.vue
@@ -0,0 +1,338 @@
+<template>
+ <div :class="classList" v-if="showField">
+ <div :class="'property-label' + activeClassName">
+ <input-field :class="labelClassName" :readonly="!editable" v-model="label"/>
+ <select-field v-if="editable" :class="activeClassName" v-model="type" :options="customTypeOptions"/>
+ </div>
+ <label v-if="labelError" class="error">{{labelErrorText}}</label>
+ <div class="property-value">
+ <a v-if="type === 'url' && !editable" :href="value">{{value}}</a>
+ <input-field ref="value" v-else :class="valueClassName" @click="copy(field.name)" :type="getInputType" v-model="value" :readonly="!editable"/>
+ <icon class="password-eye" v-if="type === 'secret'" @click="plainText = !plainText" :icon="passwordIcon" font="solid"/>
+ </div>
+ <label v-if="valueError" class="error">{{valueErrorText}}</label>
+ </div>
+</template>
+
+<script>
+ import InputField from '@vue/Components/Form/InputField';
+ import ToastService from '@js/Services/ToastService';
+ import MessageService from '@js/Services/MessageService';
+ import ErrorManager from '@js/Manager/ErrorManager';
+ import SelectField from '@vue/Components/Form/SelectField';
+ import Icon from "@vue/Components/Icon";
+ import LocalisationService from '@js/Services/LocalisationService';
+
+ export default {
+ components: { Icon, InputField, SelectField },
+ props : {
+ field : {
+ type : Object
+ },
+ editable : {
+ type : Boolean
+ },
+ maxLength: {
+ type : Boolean
+ }
+ },
+
+ data() {
+ return {
+ value : this.field.value,
+ label : this.getLabel(),
+ type : this.getType(),
+ plainText : false,
+ labelError: false,
+ valueError: false
+ };
+ },
+
+ computed: {
+ customTypeOptions() {
+ return [
+ {
+ id : 'text',
+ label: 'PasswordCustomFieldsTypeText'
+ },
+ {
+ id : 'secret',
+ label: 'PasswordCustomFieldsTypeSecret'
+ },
+ {
+ id : 'email',
+ label: 'PasswordCustomFieldsTypeEmail'
+ },
+ {
+ id : 'url',
+ label: 'PasswordCustomFieldsTypeUrl',
+ },
+ {
+ id : 'formfield',
+ label: 'PasswordCustomFieldsTypeFormField',
+ }
+ ];
+ },
+ labelClassName() {
+ var name = "label" + (this.editable === true ? " active": "");
+ name += this.labelError === true ? " error": "";
+ return name;
+ },
+ valueClassName() {
+ var name = this.editable === true ? "active": "";
+ name += this.valueError === true ? " error": "";
+ name += this.type !== "url" && this.editable === false ? " allow-copy": "";
+ return name;
+ },
+ activeClassName() {
+ return this.editable === true ? " active": "";
+ },
+ showField() {
+ if(this.type === "file") return false;
+ if(this.type === "data" && !this.value.startsWith("ext:field/")) return false;
+ if(this.editable || this.value !== '') {
+ return true;
+ }
+ return false;
+ },
+ classList() {
+ var result = "password-view-customproperty";
+ if(!this.editable) {
+ result += " readonly";
+ }
+ return result;
+ },
+ passwordIcon() {
+ if(this.plainText) {
+ return "eye-slash";
+ }
+ return "eye";
+ },
+ getInputType() {
+ if(this.type === "secret" && this.plainText !== true) {
+ return "password";
+ }
+ return "text";
+ },
+ labelErrorText() {
+ return LocalisationService.translate([`PasswordEditMaxAllowedCharacter`, this.getMaxLength('label', this.label.length)]);
+ },
+ valueErrorText() {
+ if(!this.validateUrl()) {
+ return LocalisationService.translate(`PasswordEditInvalidValue`);
+ }
+ if(!this.validateEmail()) {
+ return LocalisationService.translate(`PasswordEditInvalidValue`);
+ }
+ return LocalisationService.translate([`PasswordEditMaxAllowedCharacter`, this.getMaxLength(this.type, this.value.length)]);
+ }
+ },
+
+ methods: {
+ copy() {
+ if(this.editable) return;
+ var type = (this.type === "secret" ? "password":"text");
+ let data = this.value;
+ MessageService.send({type: 'clipboard.write', payload: {type: type, value: data}}).catch(ErrorManager.catch);
+
+ ToastService.success(['PasswordPropertyCopied', this.field.label])
+ .catch(ErrorManager.catch);
+ },
+ getType() {
+ if(this.field.type === 'data' && this.field.label.startsWith('ext:field/')) {
+ return 'formfield';
+ }
+ return this.field.type;
+ },
+ getLabel() {
+ if(this.field.type === 'data' && this.field.label.startsWith('ext:field/')) {
+ return this.field.label.replace('ext:field/', '');
+ }
+ return this.field.label;
+ },
+ validateUrl(url) {
+ if(this.type !== "url") return true;
+ var urlRegex = /^(https?|ftps?|ssh):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
+ var uncRegex = /^\\\\([^\\:\|\[\]\/";<>+=,?* _]+)\\([\u0020-\u0021\u0023-\u0029\u002D-\u002E\u0030-\u0039\u0040-\u005A\u005E-\u007B\u007E-\u00FF]{1,80})(((?:\\[\u0020-\u0021\u0023-\u0029\u002D-\u002E\u0030-\u0039\u0040-\u005A\u005E-\u007B\u007E-\u00FF]{1,255})+?|)(?:\\((?:[\u0020-\u0021\u0023-\u0029\u002B-\u002E\u0030-\u0039\u003B\u003D\u0040-\u005B\u005D-\u007B]{1,255}){1}(?:\:(?=[\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]|\:)(?:([\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]+(?!\:)|[\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]*)(?:\:([\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]+)|))|)))|)$/;
+
+ if(urlRegex.test(this.value) || uncRegex.test(this.value)) {
+ return true;
+ }
+ return false;
+ },
+ validateEmail() {
+ if(this.type !== "email") return true;
+ const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ return re.test(String(this.value).toLowerCase());
+ },
+ getMaxLength(type, length) {
+ switch (type) {
+ case 'secret': var typeLength = 256; break;
+ case 'label': var typeLength = 48; break;
+ default: var typeLength = 320;
+ }
+ var maxAllowedField = typeLength - length;
+ maxAllowedField = (maxAllowedField < this.maxLength ? maxAllowedField:this.maxLength);
+ return maxAllowedField + length;
+ },
+ updateLabel() {
+ if(this.getMaxLength('label', this.label.length) >= this.label.length) {
+ this.labelError = false;
+ this.field.label = this.label;
+ return true;
+ }
+ this.labelError = true;
+ return false;
+ },
+ updateValue() {
+ if(this.validateEmail() && this.validateUrl() ) {
+ if(this.getMaxLength(this.type, this.value.length) >= this.value.length)
+ this.valueError = false;
+ this.field.value = this.value;
+ return true;
+ }
+ this.valueError = true;
+ return false;
+ },
+ updateInput() {
+ if(this.updateValue() && this.updateLabel()) {
+ if(this.type === 'formfield') {
+ this.field.label = 'ext:field/' + this.label.replace('ext:field/', '');
+ this.field.type = 'data';
+ } else {
+ this.field.type = this.type;
+ }
+
+ this.$emit('updateField');
+ this.$emit('error', this.field, false);
+ } else {
+ this.$emit('error', this.field, true)
+ }
+
+ }
+ },
+
+ watch : {
+ value(value) {
+ if(value === undefined || value === null) return;
+ this.updateInput();
+ },
+ label(label) {
+ if(label === undefined || label === null) return;
+ this.updateInput();
+ },
+ type(type) {
+ if(type === undefined || type === null) return;
+ this.updateInput();
+ },
+ editable(value) {
+ if(value === false) {
+ this.plainText = false;
+ }
+ }
+ }
+
+ };
+</script>
+
+<style lang="scss">
+.password-view-customproperty {
+ padding : .5rem;
+ cursor : initial;
+
+ .property-label {
+ display : flex;
+ flex-direction : row;
+ justify-content : space-between;
+ line-height : 1rem;
+
+ &.active {
+ line-height : 2rem;
+ margin-bottom : .25rem;
+ }
+
+ .input-select {
+ padding : 0;
+ position : relative;
+ top : -.25rem;
+
+ select {
+ padding : 0 1.5rem 0 0;
+ }
+
+ &.active{
+ padding : .25rem;
+ top : 0;
+
+ select {
+ padding : .25rem 1.75rem .25rem .25rem;
+ }
+ }
+ }
+
+ input.label {
+ font-weight : 550;
+ padding : 0;
+ line-height : 1rem;
+ cursor : default;
+
+ &.active {
+ padding : .25rem;
+ font-weight : initial;
+ line-height : 2rem;
+ cursor : initial;
+ }
+ }
+ }
+
+
+ .property-value {
+ display : flex;
+ flex-direction : row;
+ position : relative;
+ }
+
+ .password-eye {
+ cursor : pointer;
+ position : absolute;
+ right : 0rem;
+ top : .7rem;
+ background-color : var(--element-hover-bg-color);
+ }
+
+ .input-select {
+ margin-left : .25rem;
+ }
+
+ input.active, .label.active, .input-select.active {
+ box-shadow : 0 0 0 1px var(--element-active-fg-color);
+
+ &.error{
+ box-shadow : 1px 1px 1px 0px var(--error-bg-color);
+ border : solid;
+ border-width : .3px;
+ border-color : var(--error-bg-color);
+ }
+ }
+
+ label.error {
+ padding : .25rem;
+ color : var(--error-bg-color);
+ line-height : 1.5rem;
+
+ }
+
+
+ .readonly {
+ box-shadow : none;
+ border : none;
+ }
+
+ input.allow-copy {
+ &:hover, &:active {
+ cursor : pointer;
+ border : none;
+ }
+ }
+}
+</style> \ No newline at end of file
diff --git a/src/vue/Components/Password/Mining.vue b/src/vue/Components/Password/Mining.vue
new file mode 100644
index 0000000..da378d4
--- /dev/null
+++ b/src/vue/Components/Password/Mining.vue
@@ -0,0 +1,210 @@
+<template>
+ <div class="item-menu password-mining">
+ <translate say="MiningItemIsNew" class="create-info" v-if="item.isNew()">
+ <icon slot="before" icon="info-circle" font="solid"/>
+ </translate>
+ <translate say="MiningItemIsUpdate" class="create-info" :variables="updateVariables" v-else>
+ <icon slot="before" icon="info-circle" font="solid"/>
+ </translate>
+
+ <property :password="password" :editable="editable" :field="field" v-for="field in defaultFields" :key="field" v-on:updateField="updateField" v-on:error="handleValidationError"/>
+ <label class="custom-fields">{{customFieldsLabel}}</label>
+ <custom-property :field="field" :editable="editable" v-for="field in customFields" :key="field" v-on:updateField="updateCustomField" v-on:error="handleValidationError" :maxLength="customFieldLength"/>
+ </div>
+</template>
+
+<script>
+ import Icon from "@vue/Components/Icon";
+ import MiningItem from '@js/Models/Queue/MiningItem';
+ import Property from '@vue/Components/Password/Property';
+ import CustomProperty from '@vue/Components/Password/CustomProperty';
+ import Translate from '@vue/Components/Translate';
+ import LocalisationService from '@js/Services/LocalisationService';
+ import MessageService from '@js/Services/MessageService';
+
+ export default {
+ components: {Icon, Translate, Property, CustomProperty},
+ props : {
+ item: {
+ type: MiningItem
+ }
+ },
+
+ data() {
+ return {
+ editable : true,
+ defaultFields : this.getDefaultFields(),
+ customFields : this.getCustomFields(),
+ updatedFields : {},
+ errorQueue : [],
+ customFieldLength: 8192
+ }
+ },
+
+ computed: {
+ updateVariables() {
+ return [
+ this.item.getTask().fields.label
+ ];
+ },
+ showNewCustomField() {
+ if(this.allowNewCustomField()) {
+ return true;
+ }
+ return false;
+ },
+ customFieldsLabel() {
+ return LocalisationService.translate(`LabelCustomFields`);
+ }
+ },
+
+ methods : {
+ getDefaultFields() {
+ var fields = [];
+ this.item.listResultFields().forEach((property) => {
+ if(property === "password") {
+ fields.push(this.getFieldObject(property, "password", true, true, 256));
+ }
+ if(!this.item.isNew() && (property === "edited" || property === "created")) {
+ fields.push(this.getFieldObject(property, "datetime", false, false, undefined));
+ }
+ if(property === "label") {
+ fields.push(this.getFieldObject(property, "text", true, false, 64));
+ }
+ if(property === "notes") {
+ fields.push(this.getFieldObject(property, "textarea", true, true, 4096));
+ }
+ if(property === "url") {
+ fields.push(this.getFieldObject(property, "url", true, true, 2048));
+ }
+ if(property === "username") {
+ fields.push(this.getFieldObject(property, "text", true, true, 64));
+ }
+ if(property === "hidden") {
+ fields.push(this.getFieldObject(property, "checkbox", true, false, undefined));
+ }
+ })
+ return fields;
+ },
+ getFieldObject(property, type, editable, allowCopy, maxLength) {
+ return {
+ name : property,
+ type : type,
+ value : this.item.getResultField(property),
+ editable : editable,
+ allowCopy: allowCopy,
+ maxLength: maxLength
+ }
+ },
+ getCustomFields() {
+ var result = this.item.getResultField('customFields');
+ result.push(this.getNewCustomField());
+ this.customFieldLength = 8192 - JSON.stringify(result).length;
+ return result;
+ },
+ getNewCustomField() {
+ return (
+ {
+ label : "",
+ value : "",
+ type : "text"
+ }
+ )
+ },
+ allowNewCustomField() {
+ if(this.customFields === undefined
+ || this.customFields.length >= 20) return false;
+ var allowNew = true;
+ this.customFields.forEach((e) => {
+ if(e.label === "" && e.value === "" && e.type !== "data" && e.type !== "file") {
+ allowNew = false;
+ }
+ })
+
+ return allowNew;
+ },
+ updateField(field, value) {
+ this.item.setResultField(field, value)
+ this.update();
+ },
+ updateCustomField() {
+ this.item.setResultField('customFields', this.customFields)
+ this.update();
+ this.customFieldLength = 8192 - JSON.stringify(this.customFields).length;
+ if(this.allowNewCustomField()) {
+ this.customFields.push(this.getNewCustomField());
+ }
+ },
+ update() {
+ MessageService
+ .send({type: 'popup.mining.update', payload: this.item});
+ },
+ handleValidationError(field, error) {
+ if(this.errorQueue.indexOf(field) !== -1) {
+ if(error === false) {
+ this.errorQueue.splice(this.errorQueue.indexOf(field), 1);
+ }
+ } else {
+ if(error === true) {
+ this.errorQueue.push(field)
+ }
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+.item-menu.password-mining {
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-hover-fg-color);
+
+ .create-info {
+ display : block;
+ padding : 1rem .5rem .25rem .5rem;
+ color : var(--element-active-fg-color);
+ }
+
+ .icon {
+ text-align : center;
+ width : 3rem;
+ display : inline-block;
+ }
+
+ label.custom-fields {
+ display : block;
+ font-weight : 550;
+ line-height : 1rem;;
+ padding-left : .5rem;
+ padding-bottom : .25rem;
+ }
+
+ input, textarea {
+ width : 100%;
+ padding : .25rem;
+ box-sizing : border-box;
+ border-radius : 3px;
+ border : none;
+ line-height : 2rem;
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-fg-color);
+ scrollbar-width : thin;
+ }
+
+ input:focus,
+ select:focus,
+ textarea:focus,
+ button:focus {
+ outline: none;
+ }
+
+ a {
+ width : 100%;
+ padding : .25rem;
+ line-height : 2rem;
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-active-fg-color);
+ }
+
+}
+</style> \ No newline at end of file
diff --git a/src/vue/Components/Password/Property.vue b/src/vue/Components/Password/Property.vue
new file mode 100644
index 0000000..0e87668
--- /dev/null
+++ b/src/vue/Components/Password/Property.vue
@@ -0,0 +1,301 @@
+<template>
+ <div :class="classList" v-if="canEdit || value !== ''">
+ <div v-if="field.type === 'checkbox'" class="password-checkbox">
+ <label class="property-label">{{ label }}</label>
+ <slider-field v-model="value" :readonly="!canEdit" :class="activeClassName"/>
+ </div>
+ <label v-if="field.type !== 'checkbox'" class="property-label">{{ label }}</label>
+ <div v-if="field.type !== 'checkbox'" class="property-value">
+ <a v-if="field.type === 'url' && !canEdit" :href="value">{{text}}</a>
+ <input-field v-else-if="field.type === 'datetime' || field.type === 'folder'" v-model="text" :readonly="true" class="readonly"/>
+ <input-field v-else v-model="value" :type="getInputType" @click="copyProperty(field.name)" @dblclick="copyNotes(field.name)" :readonly="!canEdit" :class="activeClassName"/>
+ <div class="password-icon">
+ <icon v-if="editable && field.type === 'password'" @click="generatePassword" icon="sync" font="solid" :spin="generating"/>
+ <icon v-if="field.type === 'password'" @click="plainText = !plainText" :icon="passwordIcon" font="solid"/>
+ </div>
+ </div>
+ <label v-if="valueError" class="error">{{valueErrorText}}</label>
+ </div>
+</template>
+
+<script>
+ import Password from 'passwords-client/src/Model/Password/Password';
+ import InputField from '@vue/Components/Form/InputField';
+ import SliderField from '@vue/Components/Form/SliderField';
+ import ToastService from '@js/Services/ToastService';
+ import MessageService from '@js/Services/MessageService';
+ import ErrorManager from '@js/Manager/ErrorManager';
+ import LocalisationService from '@js/Services/LocalisationService';
+ import SettingsService from '@js/Services/SettingsService';
+ import Icon from "@vue/Components/Icon";
+
+ export default {
+ components: { Icon, InputField, SliderField },
+ props : {
+ password : {
+ type : Password
+ },
+ field : {
+ type : JSON
+ },
+ editable : {
+ type : Boolean
+ }
+ },
+
+ data() {
+ return {
+ value : this.field.value,
+ plainText : false,
+ folder : undefined,
+ valueError : false,
+ generating : true,
+ numbers : false,
+ special : false,
+ strength : 1,
+ };
+ },
+
+ async mounted() {
+ var response = await MessageService.send({type: 'folder.show', payload: this.value});
+ var payload = response.getPayload();
+ if(payload !== undefined && payload !== null ) {
+ this.folder = payload;
+ }
+
+ let promises = [
+ this.loadSetting('strength'),
+ this.loadSetting('numbers'),
+ this.loadSetting('special')
+ ];
+
+ Promise.all(promises).then(() => {
+ this.generating = false;
+ });
+ },
+
+ computed: {
+ canEdit() {
+ if(this.editable && this.field.editable) {
+ return true;
+ }
+ return false;
+ },
+ label() {
+ return LocalisationService.translate(`Label${this.field.name.capitalize()}`);
+ },
+ text() {
+ var type = this.field.type;
+ if(type === 'datetime') {
+ if(typeof this.value === "number") {
+ return new Date(this.value * 1000).toLocaleString();
+ } else {
+ return new Date(this.value.toString()).toLocaleString();
+ }
+ }
+ if(type === 'folder') {
+ if(this.folder === undefined) {
+ return this.value;
+ }
+
+ return this.folder.getLabel();
+ }
+ return this.value;
+ },
+ classList() {
+ var result = "password-view-property";
+ if(!this.canEdit) {
+ result += " readonly";
+ }
+ if(this.field.allowCopy && this.editable === false) {
+ result += " allow-copy";
+ }
+ return result;
+ },
+ getInputType() {
+ if(this.field.type === "textarea") {
+ return "textarea";
+ }
+ if(this.field.type === "password" && this.plainText !== true) {
+ return "password";
+ }
+ return "text";
+ },
+ activeClassName() {
+ var result = this.editable === true ? " active": "";
+ result += this.field.type === 'password' ? ' password-edit':'';
+ return result;
+ },
+ passwordIcon() {
+ if(this.plainText) {
+ return "eye-slash";
+ }
+ return "eye";
+ },
+ valueErrorText() {
+ if(!this.validateUrl()) {
+ return LocalisationService.translate(`PasswordEditInvalidValue`);
+ }
+ return LocalisationService.translate([`PasswordEditMaxAllowedCharacter`, this.field.maxLength]);
+ }
+ },
+
+ methods: {
+ copy(property) {
+ let data = this.field.value;
+ MessageService.send({type: 'clipboard.write', payload: {type: this.field.type, value: data}}).catch(ErrorManager.catch);
+
+ let label = LocalisationService.translate(`Label${property.capitalize()}`);
+ ToastService.success(['PasswordPropertyCopied', label])
+ .catch(ErrorManager.catch);
+ },
+ copyProperty(property) {
+ if(this.field.allowCopy == false || this.editable === true || property === 'notes') return;
+ this.copy(property);
+ },
+ copyNotes(property) {
+ if(this.editable === true || property !== 'notes') return;
+ this.copy(property);
+ },
+ async loadSetting(type) {
+ this[type] = await SettingsService.getValue(`password.generator.${type}`);
+ },
+ async generatePassword() {
+ if(this.generating) return;
+ this.generating = true;
+ let response = /** @type {Message} **/ await MessageService
+ .send({type: 'password.generate', payload: {numbers: this.numbers, special: this.special, strength: this.strength}});
+ let data = response.getPayload();
+ if(data.success) {
+ this.value = data.password;
+ }
+ this.generating = false;
+ },
+ validateLength() {
+ if(this.field.maxLength === undefined) return true;
+ if(this.field.maxLength <= this.value.length) {
+ return false;
+ }
+ return true;
+ },
+ validateUrl() {
+ if(this.field.type !== "url") return true;
+ var urlRegex = /^(https?|ftps?|ssh):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
+ var uncRegex = /^\\\\([^\\:\|\[\]\/";<>+=,?* _]+)\\([\u0020-\u0021\u0023-\u0029\u002D-\u002E\u0030-\u0039\u0040-\u005A\u005E-\u007B\u007E-\u00FF]{1,80})(((?:\\[\u0020-\u0021\u0023-\u0029\u002D-\u002E\u0030-\u0039\u0040-\u005A\u005E-\u007B\u007E-\u00FF]{1,255})+?|)(?:\\((?:[\u0020-\u0021\u0023-\u0029\u002B-\u002E\u0030-\u0039\u003B\u003D\u0040-\u005B\u005D-\u007B]{1,255}){1}(?:\:(?=[\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]|\:)(?:([\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]+(?!\:)|[\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]*)(?:\:([\u0001-\u002E\u0030-\u0039\u003B-\u005B\u005D-\u00FF]+)|))|)))|)$/;
+
+ if(urlRegex.test(this.value) || uncRegex.test(this.value)) {
+ return true;
+ }
+ return false;
+ }
+ },
+
+ watch : {
+ value(value) {
+ if(value === undefined || value === null) return;
+ if(this.validateLength() && this.validateUrl()) {
+ this.valueError = false;
+ this.$emit('updateField', this.field.name, value);
+ this.$emit('error', this.field, false);
+ } else {
+ this.$emit('error', this.field, true)
+ this.valueError = true;
+ }
+ },
+ editable(value) {
+ if(value === false) {
+ this.plainText = false;
+ }
+ }
+ }
+
+ };
+</script>
+
+<style lang="scss">
+.password-view-property {
+ padding : .5rem;
+ cursor : initial;
+
+ padding : .5rem;
+ cursor : initial;
+
+ .property-label {
+ display : block;
+ font-weight : 550;
+ line-height : 1rem;
+ }
+
+ .property-value {
+ display : flex;
+ flex-direction : row;
+ position : relative;
+ }
+
+ .password-icon {
+ cursor : pointer;
+ position : absolute;
+ right : .5rem;
+ top : .75rem;
+ background-color : var(--element-hover-bg-color);
+
+ span.icon {
+ width : 1.75rem;
+ }
+ }
+
+ input.password-edit {
+ padding-right: 2.5rem !important;
+
+ &.active {
+ padding-right: 4.5rem !important;
+ }
+ }
+
+
+ input.active, textarea.active, .label.active {
+ box-shadow : 0 0 0 1px var(--element-active-fg-color);
+
+ &.error{
+ box-shadow : 1px 1px 1px 0px var(--error-bg-color);
+ border : solid;
+ border-width : .3px;
+ border-color : var(--error-bg-color);
+ }
+ }
+
+ label.error {
+ padding : .25rem;
+ color : var(--error-bg-color);
+ line-height : 1.5rem;
+
+ }
+
+ .readonly {
+ box-shadow : none;
+ border : none;
+ }
+
+ &.allow-copy {
+ input:hover, input:active {
+ cursor : pointer;
+ border : none;
+ }
+ }
+
+ .password-checkbox {
+ display : flex;
+ flex-direction : row;
+ justify-content : space-between;
+ padding-right : 1rem;
+
+ .input-slider {
+ cursor : default;
+
+ &.active {
+ cursor : pointer;
+ }
+ }
+ }
+}
+</style> \ No newline at end of file
diff --git a/src/vue/Components/Password/View.vue b/src/vue/Components/Password/View.vue
new file mode 100644
index 0000000..bffb947
--- /dev/null
+++ b/src/vue/Components/Password/View.vue
@@ -0,0 +1,315 @@
+<template>
+ <div class="item-menu password-view">
+ <div class="password-header">
+ <div class="left-space"/>
+ <div class="badge-container">
+ <icon :class="securityClass" icon="shield-alt" font="solid"/>
+ <icon class="favorite" @click="updateFavorite()" icon="star" :font="favoriteIconSolid"/>
+ <icon :icon="sharedIcon" font="solid"/>
+ </div>
+ <div class="action-icon">
+ <icon v-if="password.getProperty('editable')" :icon="actionIcon" @click="toggleAction()" :class="actionClassList"/>
+ </div>
+ </div>
+ <property :editable="editable" :field="field" v-for="field in defaultFields" :key="field" v-on:updateField="updateField" v-on:error="handleValidationError"/>
+ <label v-if="customFields.length > 1" class="custom-fields">{{customFieldsLabel}}</label>
+ <custom-property :field="field" :editable="editable" v-for="field in customFields" :key="field" v-on:updateField="updateCustomField" v-on:error="handleValidationError" :maxLength="customFieldLength"/>
+ </div>
+</template>
+
+<script>
+ import Icon from "@vue/Components/Icon";
+ import Password from "passwords-client/src/Model/Password/Password";
+ import Property from '@vue/Components/Password/Property';
+ import CustomProperty from '@vue/Components/Password/CustomProperty';
+ import MessageService from "@js/Services/MessageService";
+ import LocalisationService from '@js/Services/LocalisationService';
+
+ export default {
+ components: {Icon, Property, CustomProperty},
+ props : {
+ password: {
+ type: Password
+ }
+ },
+
+ data() {
+ return {
+ editable : false,
+ defaultFields : this.getDefaultFields(),
+ customFields : this.getCustomFields(),
+ updatedFields : {},
+ errorQueue : [],
+ customFieldLength: 8192
+ }
+ },
+
+ computed: {
+ securityClass() {
+ let types = ['secure', 'warn', 'bad'];
+
+ return `security ${types[this.password.getStatus()]}`;
+ },
+ favoriteIconSolid() {
+ if(this.password.getProperty('favorite') === true) {
+ return "solid";
+ }
+ return "reqular";
+ },
+ sharedIcon() {
+ if(this.password.getProperty('shared') === true) {
+ return "users";
+ }
+ return "user-shield";
+ },
+ showNewCustomField() {
+ if(this.allowNewCustomField()) {
+ return true;
+ }
+ return false;
+ },
+ actionClassList() {
+ return this.editable === true && this.errorQueue.length > 0 ? "action-icon disabled":"action-icon";
+ },
+ actionIcon() {
+ return this.editable ? "save":"edit";
+ },
+ customFieldsLabel() {
+ return LocalisationService.translate(`LabelCustomFields`);
+ }
+ },
+
+ methods : {
+ getDefaultFields() {
+ var fields = [];
+ for (var property in this.password.getProperties()) {
+ if(property === "password") {
+ fields.push(this.getFieldObject(property, "password", true, true, 256));
+ }
+ if(property === "edited" || property === "created") {
+ fields.push(this.getFieldObject(property, "datetime", false, false, undefined));
+ }
+ if(property === "label") {
+ fields.push(this.getFieldObject(property, "text", true, false, 64));
+ }
+ if(property === "folder") {
+ fields.push(this.getFieldObject(property, "folder", false, false, undefined));
+ }
+ if(property === "notes") {
+ fields.push(this.getFieldObject(property, "textarea", true, true, 4096));
+ }
+ if(property === "url") {
+ fields.push(this.getFieldObject(property, "url", true, true, 2048));
+ }
+ if(property === "username") {
+ fields.push(this.getFieldObject(property, "text", true, true, 64));
+ }
+ if(property === "hidden") {
+ fields.push(this.getFieldObject(property, "checkbox", true, false, undefined));
+ }
+ }
+ return fields;
+ },
+ getFieldObject(property, type, editable, allowCopy, maxLength) {
+ return {
+ name : property,
+ type : type,
+ value : this.password.getProperty(property),
+ editable : editable,
+ allowCopy: allowCopy,
+ maxLength: maxLength
+ }
+ },
+ getCustomFields() {
+ var result = this.password.getProperty('customFields');
+ result.push(this.getNewCustomField());
+ this.customFieldLength = 8192 - JSON.stringify(result).length;
+ return result;
+ },
+ getNewCustomField() {
+ return (
+ {
+ label : "",
+ value : "",
+ type : "text"
+ }
+ )
+ },
+ allowNewCustomField() {
+ if(this.customFields === undefined
+ || this.customFields.length >= 20) return false;
+ if(this.updatedFields !== undefined
+ && this.updatedFields.customFields !== undefined
+ && this.updatedFields.customFields.length >=20) return false;
+ return true;
+ },
+ async updateFavorite() {
+ var data = {
+ id: this.password.getId(),
+ favorite: !this.password.getFavorite()
+ }
+ var result = await MessageService.send({type: 'password.update', payload: {data: data}});
+ if(result.getPayload().success === true) {
+ this.password.setProperties(result.getPayload().data);
+ }
+ },
+ toggleAction() {
+ if(this.errorQueue.length > 0) return;
+ if(this.editable === true) {
+ this.save();
+ }
+ this.editable = !this.editable;
+ },
+ save() {
+ if(Object.keys(this.updatedFields).length !== 0) {
+ this.removeEmptyCustomFields();
+ this.updatedFields.id = this.password.getId();
+ MessageService.send({type: 'password.update', payload: {data: this.updatedFields}});
+ this.updatedFields = {};
+ }
+ },
+ updateField(field, value) {
+ this.updatedFields[field] = value;
+ },
+ updateCustomField() {
+ this.updatedFields.customFields = this.customFields;
+ this.customFieldLength = 8192 - JSON.stringify(this.customFields).length;
+ if(!this.allowNewCustomField()) return;
+ var emptyFieldAvailable = false;
+ this.updatedFields.customFields.forEach((e) => {
+ if(e.label === "" && e.value === "" && e.type !== "data" && e.type !== "file") {
+ emptyFieldAvailable = true;
+ }
+ })
+ if(!emptyFieldAvailable) {
+ this.customFields.push(this.getNewCustomField());
+ }
+ },
+ removeEmptyCustomFields(){
+ if(this.updatedFields.customFields === undefined) return;
+ this.updatedFields.customFields.forEach((e) => {
+ if((e.label === "" && e.value === "" && e.type !== "data" && e.type !== "file")
+ || e.label === "ext:field/" && e.type === 'data') {
+
+ var i = this.updatedFields.customFields.indexOf(e)
+ this.updatedFields.customFields.splice(i, 1);
+ }
+ })
+ },
+ handleValidationError(field, error) {
+ if(this.errorQueue.indexOf(field) !== -1) {
+ if(error === false) {
+ this.errorQueue.splice(this.errorQueue.indexOf(field), 1);
+ }
+ } else {
+ if(error === true) {
+ this.errorQueue.push(field)
+ }
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+.item-menu.password-view {
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-hover-fg-color);
+
+ .icon {
+ text-align : center;
+ width : 3rem;
+ display : inline-block;
+ }
+
+ .password-header {
+ line-height : 3rem;
+ display : flex;
+ justify-content: space-between;
+ margin-bottom : -1rem;
+
+ .left-space, .action-icon {
+ width : 3rem
+ }
+
+ .badge-container {
+ .security {
+ &.secure {
+ color : var(--success-bg-color)
+ }
+
+ &.warn {
+ color : var(--warning-bg-color)
+ }
+
+ &.bad {
+ color : var(--error-bg-color)
+ }
+ }
+
+ .favorite {
+ cursor : pointer;
+ color : var(--warning-bg-color);
+ font-size : calc(var(--font-size) + 1rem);
+ position : relative;
+ bottom : -.25rem;
+ }
+ }
+
+ .action-icon {
+
+ &.icon.disabled {
+ opacity : .5;
+
+ &:hover {
+ cursor : initial;
+ background-color : initial;
+ color : initial;
+ }
+ }
+
+ &.icon:hover {
+ cursor : pointer;
+ background-color : var(--button-hover-bg-color);
+ color : var(--button-hover-fg-color);
+ }
+ }
+ }
+
+ label.custom-fields {
+ display : block;
+ font-weight : 550;
+ line-height : 1rem;;
+ padding-left : .5rem;
+ padding-bottom : .25rem;
+ }
+
+ input, textarea {
+ width : 100%;
+ padding : .25rem;
+ box-sizing : border-box;
+ border-radius : 3px;
+ border : none;
+ line-height : 2rem;
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-fg-color);
+ scrollbar-width : thin;
+ }
+
+ input:focus,
+ select:focus,
+ textarea:focus,
+ button:focus {
+ outline: none;
+ }
+
+ a {
+ width : 100%;
+ padding : .25rem;
+ line-height : 2rem;
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-active-fg-color);
+ }
+
+}
+</style> \ No newline at end of file
diff --git a/src/vue/Components/Popup/Collected.vue b/src/vue/Components/Popup/Collected.vue
index 03f1f01..56de5ff 100644
--- a/src/vue/Components/Popup/Collected.vue
+++ b/src/vue/Components/Popup/Collected.vue
@@ -5,7 +5,7 @@
<icon icon="trash-alt" @click="discard(item)"/>
<icon icon="save" @click="save(item)"/>
</div>
- <mining-item :item="item" :slot="item.getId()" :key="item.getId()" v-for="item of items"/>
+ <password-mining :item="item" :slot="item.getId()" :key="item.getId()" v-for="item of items"/>
</foldout>
<translate tag="div" class="no-results" say="NoCollectedPasswords" v-if="items.length === 0"/>
<translate class="collected-add-blank" say="AddPasswordForCurrentTab" @click="addBlankPassword" v-if="items.length === 0">
@@ -19,12 +19,12 @@
import Foldout from '@vue/Components/Foldout';
import Translate from '@vue/Components/Translate';
import MiningClient from '@js/Queue/Client/MiningClient';
- import MiningItem from '@vue/Components/Collected/MiningItem';
+ import PasswordMining from '@vue/Components/Password/Mining';
import MessageService from '@js/Services/MessageService';
import ErrorManager from '@js/Manager/ErrorManager';
export default {
- components: {MiningItem, Translate, Foldout, Icon},
+ components: {Translate, Foldout, Icon, PasswordMining},
props: {
initialStatus: {