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
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/Controller/Password/Update.js63
-rw-r--r--src/js/Manager/ControllerManager.js7
-rw-r--r--src/platform/generic/_locales/de/messages.json44
-rw-r--r--src/platform/generic/_locales/en/messages.json44
-rw-r--r--src/vue/App/Popup.vue7
-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.vue26
-rw-r--r--src/vue/Components/Password/CustomProperty.vue244
-rw-r--r--src/vue/Components/Password/Property.vue224
-rw-r--r--src/vue/Components/Password/View.vue253
13 files changed, 932 insertions, 11 deletions
diff --git a/src/js/Controller/Password/Update.js b/src/js/Controller/Password/Update.js
new file mode 100644
index 0000000..3d393dc
--- /dev/null
+++ b/src/js/Controller/Password/Update.js
@@ -0,0 +1,63 @@
+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 ServerManager from '@js/Manager/ServerManager';
+
+export default class Update extends AbstractController {
+
+ async execute(message, reply) {
+ let {data} = message.getPayload();
+ data.edited = new Date();
+
+ if(data.id === undefined) {
+ this._returnError();
+ return;
+ }
+
+ /** @type {EnhancedPassword} **/
+ let model = SearchIndex.getItem(data.id);
+ model.setProperties(data);
+
+ if(model !== null && !model.isTrashed()) {
+ await this._updatePassword(model, data.customFields === undefined ? false:true);
+ reply.setPayload({success: true, data: data});
+ } else {
+ reply.setPayload({success: false});
+ this._returnError()
+ }
+ }
+
+
+ /**
+ *
+ * @param {EnhancedPassword} password
+ * @private
+ */
+ async _updatePassword(password, reloadServer) {
+ let api = /** @type {PasswordsClient} **/ await ApiRepository.findById(password.getServer().getId());
+ let repository = /** @type {PasswordRepository} **/ api.getInstance('repository.password');
+ await repository.update(password);
+
+ if(reloadServer === true) {
+ ServerManager.reloadServer(password.getServer());
+ } else {
+ 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/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/platform/generic/_locales/de/messages.json b/src/platform/generic/_locales/de/messages.json
index ff73dd4..ba330d6 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,22 @@
"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."
+ },
"LabelTags" : {
"message" : "Tags",
"description": "Label for the tag count in the server info in the browse tab"
@@ -641,6 +661,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",
@@ -1310,5 +1338,21 @@
"SettingsShowUsernameInList" : {
"message" : "Zeige Benutzername neben dem Titel",
"description": "Label of the setting in the extension settings to show the username next to the title in password lists."
+ },
+ "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'."
}
}
diff --git a/src/platform/generic/_locales/en/messages.json b/src/platform/generic/_locales/en/messages.json
index c535582..cc7cc38 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,22 @@
"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."
+ },
"LabelTags" : {
"message" : "Tags",
"description": "Label for the tag count in the server info in the browse tab"
@@ -665,6 +685,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",
@@ -1324,5 +1352,21 @@
"SettingsShowUsernameInList" : {
"message" : "Show username next to title",
"description": "Label of the setting in the extension settings to show the username next to the title in password lists."
+ },
+ "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'."
}
}
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/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 cffad61..c0dd21c 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">
<favicon :password="password.getId()" :size="22" v-if="favicon"/>
{{ label }}
@@ -8,11 +8,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>
@@ -28,9 +29,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
@@ -47,8 +49,9 @@
data() {
return {
- active : true,
- showMenu: false
+ active : true,
+ showMenu : false,
+ showEntry: false
};
},
@@ -127,6 +130,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..1b3cd0d
--- /dev/null
+++ b/src/vue/Components/Password/CustomProperty.vue
@@ -0,0 +1,244 @@
+<template>
+ <div :class="classList" v-if="showField">
+ <div :class="'property-label' + activeClassName">
+ <label v-if="!editable">{{label}}</label>
+ <input-field v-else :class="labelClassName" v-model="label"/>
+ <select-field :class="activeClassName" v-model="type" :options="customTypeOptions" :disabled="!editable"/>
+ </div>
+ <div class="property-value">
+ <a v-if="field.type === 'url' && !editable" :href="value">{{text}}</a>
+ <input-field v-else :class="activeClassName" @click="copy(field.name)" :type="getInputType" v-model="value" :readonly="!editable"/>
+ <div class="password-eye">
+ <icon v-if="field.type === 'secret'" @click="plainText = !plainText" :icon="passwordIcon" font="solid"/>
+ </div>
+ </div>
+ </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";
+
+ export default {
+ components: { Icon, InputField, SelectField },
+ props : {
+ field : {
+ type : Object
+ },
+ editable : {
+ type : Boolean
+ }
+ },
+
+ data() {
+ return {
+ value : this.field.value,
+ label : this.field.label,
+ type : this.field.type,
+ plainText: false
+ };
+ },
+
+ computed: {
+ customTypeOptions() {
+ return [
+ {
+ id : 'text',
+ label: 'PasswordCustomFieldsTypeText'
+ },
+ {
+ id : 'secret',
+ label: 'PasswordCustomFieldsTypeSecret'
+ },
+ {
+ id : 'email',
+ label: 'PasswordCustomFieldsTypeEmail'
+ },
+ {
+ id : 'url',
+ label: 'PasswordCustomFieldsTypeUrl',
+ }
+ ];
+ },
+ labelClassName() {
+ return "label" + (this.editable === true ? " active": "");
+ },
+ activeClassName() {
+ return this.editable === true ? " active": "";
+ },
+ showField() {
+ if(this.field.type === "file") return false;
+ if(this.field.type === "data") return false;
+ if(this.editable || this.value !== '') {
+ return true;
+ }
+ return false;
+ },
+ label() {
+ return this.field.label;
+ },
+ classList() {
+ var result = "password-view-customproperty";
+ if(!this.editable) {
+ result += " readonly";
+ }
+ if(this.field.type !== "url" && this.editable === false) {
+ result += " allow-copy";
+ }
+ return result;
+ },
+ passwordIcon() {
+ if(this.plainText) {
+ return "eye-slash";
+ }
+ return "eye";
+ },
+ getInputType() {
+ if(this.field.type === "secret" && this.plainText !== true) {
+ return "password";
+ }
+ return "text";
+ }
+ },
+
+ methods: {
+ copy() {
+ if(this.editable) return;
+ var type = (this.field.type === "secret" ? "password":"text");
+ let data = this.field.value;
+ MessageService.send({type: 'clipboard.write', payload: {type: type, value: data}}).catch(ErrorManager.catch);
+
+ ToastService.success(['PasswordPropertyCopied', this.field.label])
+ .catch(ErrorManager.catch);
+ },
+ },
+ watch : {
+ value(value) {
+ if(value === undefined || null) return;
+ this.field.value = value;
+ this.$emit('updateField');
+ },
+ label(label) {
+ if(label === undefined || null) return;
+ this.field.label = label;
+ this.$emit('updateField');
+ },
+ type(type) {
+ if(type === undefined || null) return;
+ this.field.type = type;
+ this.$emit('updateField');
+ },
+ 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;
+ }
+ }
+ }
+
+ label {
+ font-weight : 550;
+ }
+
+ }
+
+
+ .property-value {
+ display : flex;
+ flex-direction : row;
+ position : relative;
+ }
+
+ .password-eye {
+ width : 0;
+ cursor : pointer;
+
+ .icon {
+ position : absolute;
+ right : 1rem;
+ top : .7rem;
+ }
+ }
+
+ .input-select {
+ margin-left : .25rem;
+ }
+
+ input {
+ 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);
+ }
+
+ input.active, .label.active, .input-select.active {
+ box-shadow : 0 0 0 1px var(--element-active-fg-color);
+
+ }
+
+ a {
+ width : 100%;
+ padding : .25rem;
+ line-height : 2rem;
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-active-fg-color);
+ }
+
+ .readonly {
+ box-shadow : none;
+ border : none;
+ }
+
+ &.allow-copy {
+ input:hover, input:active {
+ cursor : pointer;
+ border : none;
+ }
+ }
+}
+</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..5e81a82
--- /dev/null
+++ b/src/vue/Components/Password/Property.vue
@@ -0,0 +1,224 @@
+<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>
+ <div v-else>
+ <label class="property-label">{{ label }}</label>
+ <div 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-eye">
+ <icon v-if="field.type === 'password'" @click="plainText = !plainText" :icon="passwordIcon" font="solid"/>
+ </div>
+ </div>
+ </div>
+ </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 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.password.getProperty(this.field.name),
+ plainText: false,
+ folder: undefined
+ };
+ },
+
+ 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;
+ }
+ },
+
+ 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') {
+ return new Date(this.value * 1000).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() {
+ return this.editable === true ? " active": "";
+ },
+ passwordIcon() {
+ if(this.plainText) {
+ return "eye-slash";
+ }
+ return "eye";
+ }
+ },
+
+ methods: {
+ copy(property) {
+ let data = this.password.getProperty(property);
+ 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);
+ }
+ },
+
+ watch : {
+ value(value) {
+ if(value === undefined || value === null) return;
+ this.$emit('updateField', this.field.name, value);
+ },
+ 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-eye {
+ width : 0;
+ cursor : pointer;
+
+ .icon {
+ position : absolute;
+ right : 1rem;
+ top : .7rem;
+ }
+ }
+
+ 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.active, textarea.active, .label.active {
+ box-shadow : 0 0 0 1px var(--element-active-fg-color);
+
+ }
+
+ a {
+ width : 100%;
+ padding : .25rem;
+ line-height : 2rem;
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-active-fg-color);
+ }
+
+ .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;
+ }
+}
+</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..57aafdf
--- /dev/null
+++ b/src/vue/Components/Password/View.vue
@@ -0,0 +1,253 @@
+<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-container">
+ <icon icon="save" @click="save()" v-if="editable"/>
+ <icon icon="edit" @click="editable = !editable" v-if="!editable"/>
+ </div>
+ </div>
+ <div class="password-view-item">
+ <property :password="password" :editable="editable" :field="field" v-for="field in defaultFields" :key="field" v-on:updateField="updateField"/>
+ <custom-property :field="field" :editable="editable" v-for="field in customFields" :key="field" v-on:updateField="updateCustomField"/>
+ </div>
+ </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";
+
+ export default {
+ components: {Icon, Property, CustomProperty},
+ props : {
+ password: {
+ type: Password
+ }
+ },
+
+ data() {
+ return {
+ editable : false,
+ defaultFields : this.getDefaultFields(),
+ customFields : this.getCustomFields(),
+ updatedFields : {}
+ }
+ },
+
+ 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;
+ }
+ },
+
+ methods : {
+ getDefaultFields() {
+ var fields = [];
+ for (var property in this.password.getProperties()) {
+ if(property === "password") {
+ fields.push(this.getFieldObject(property, "password", true, true));
+ }
+ if(property === "edited" || property === "created") {
+ fields.push(this.getFieldObject(property, "datetime", false, false));
+ }
+ if(property === "label") {
+ fields.push(this.getFieldObject(property, "text", true, false));
+ }
+ if(property === "folder") {
+ fields.push(this.getFieldObject(property, "folder", false, false));
+ }
+ if(property === "notes") {
+ fields.push(this.getFieldObject(property, "textarea", true, true));
+ }
+ if(property === "url") {
+ fields.push(this.getFieldObject(property, "url", true, true));
+ }
+ if(property === "username") {
+ fields.push(this.getFieldObject(property, "text", true, true));
+ }
+ if(property === "hidden") {
+ fields.push(this.getFieldObject(property, "checkbox", true, false));
+ }
+ }
+ return fields;
+ },
+ getFieldObject(property, type, editable, allowCopy) {
+ return {
+ name: property,
+ type: type,
+ editable: editable,
+ allowCopy: allowCopy,
+ }
+ },
+ getCustomFields() {
+ var result = this.password.getProperty('customFields');
+ result.push(this.getNewCustomField());
+ 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);
+ }
+ },
+ 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 = {};
+ }
+ this.editable = false;
+ },
+ updateField(field, value) {
+ this.updatedFields[field] = value;
+ },
+ updateCustomField() {
+ this.updatedFields.customFields = this.customFields;
+ 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") {
+ var i = this.updatedFields.customFields.indexOf(e)
+ this.updatedFields.customFields.splice(i, 1);
+ }
+ })
+ }
+ }
+ };
+</script>
+
+<style lang="scss">
+.item-menu.password-view {
+ background-color : var(--element-hover-bg-color);
+ color : var(--element-hover-fg-color);
+
+ .password-header {
+ line-height : 3rem;
+ display : flex;
+ justify-content: space-between;
+ margin-bottom : -1rem;
+
+ .left-space{
+ width : 3rem
+ }
+ .icon {
+ width : 3rem;
+ display : inline-block;
+ text-align : center;
+ }
+
+ .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-container {
+ cursor : pointer;
+
+ .icon:hover {
+ background-color : var(--button-hover-bg-color);
+ color : var(--button-hover-fg-color);
+ }
+ }
+ }
+
+
+ .view-item {
+ line-height : 3rem;
+ cursor : pointer;
+ display : flex;
+
+ .icon {
+ text-align : center;
+ width : 3rem;
+ display : inline-block;
+ }
+
+ &:hover {
+ background-color : var(--element-active-hover-bg-color);
+ color : var(--element-active-hover-fg-color);
+ }
+ }
+}
+</style> \ No newline at end of file