diff options
Diffstat (limited to 'src/vue')
-rw-r--r-- | src/vue/App/Popup.vue | 7 | ||||
-rw-r--r-- | src/vue/Components/Form/InputField.vue | 18 | ||||
-rw-r--r-- | src/vue/Components/Form/SliderField.vue | 9 | ||||
-rw-r--r-- | src/vue/Components/Icon.vue | 1 | ||||
-rw-r--r-- | src/vue/Components/List/Item/Menu/PasswordMenu.vue | 3 | ||||
-rw-r--r-- | src/vue/Components/List/Item/Password.vue | 26 | ||||
-rw-r--r-- | src/vue/Components/Password/CustomProperty.vue | 244 | ||||
-rw-r--r-- | src/vue/Components/Password/Property.vue | 224 | ||||
-rw-r--r-- | src/vue/Components/Password/View.vue | 253 |
9 files changed, 774 insertions, 11 deletions
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 |