diff options
-rw-r--r-- | css/ContactDetailsAvatar.scss | 89 | ||||
-rw-r--r-- | css/icons.scss | 4 | ||||
-rw-r--r-- | package-lock.json | 2 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/components/ContactDetails.vue | 5 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsAvatar.vue | 151 | ||||
-rw-r--r-- | src/main.js | 6 |
7 files changed, 166 insertions, 92 deletions
diff --git a/css/ContactDetailsAvatar.scss b/css/ContactDetailsAvatar.scss index c754abea..939d7ce9 100644 --- a/css/ContactDetailsAvatar.scss +++ b/css/ContactDetailsAvatar.scss @@ -35,8 +35,6 @@ width: 75px; height: 75px; margin-left: auto; - overflow: hidden; - border-radius: 50%; } &__background { opacity: .2; @@ -44,90 +42,51 @@ left: 0; top: 50px; } + &__photo, + &__options { + width: 100%; + height: 100%; + border-radius: 50%; + overflow: hidden; + } &__photo { z-index: 10; background-size: cover; - width: 100%; - height: 100%; background-repeat: no-repeat; background-position: center; cursor: pointer; } - label.icon-upload-force-white { - opacity: .5; + &__options { + top: 0; z-index: 2; position: absolute; + background-color: rgba(0, 0, 0, 0.2); + } + .contact-avatar-options { width: 100%; height: 100%; + display: block; + opacity: .5; + background-color: rgba(0, 0, 0, 0.2); &:hover, &:active, &:focus { opacity: .7; } + &__popovermenu { + top: calc(50% + 24px); + right: calc(50% - 22px); + } } - &__photo + label.icon-upload-force-white { - opacity: 0; - } - &__options { - top: 0; - background-color: rgba(0, 0, 0, 0.2); - width: 100%; - height: 100%; - overflow: hidden; + // if picture is set, hide the menu + &__photo + &__options { + z-index: -1; } } -.contact-header-avatar.maximised { - .contact-header-avatar__wrapper { - position: fixed; - height: 100%; - width: 100%; - top: 0; - left: 0; - border-radius: 0; - margin: 0px; - background-color: rgba(0, 0, 0, 0.9); +.contact-header-modal__photo { + .modal-container { display: flex; - justify-content: center; - z-index: 200; - padding-top: $header-height; - flex-direction: column; - .contact-header-avatar__photo { - border-radius: 0; - align-self: center; - background-size: contain; - margin: 50px; - } - label.icon-upload-force-white { - position: relative; - } - .contact-header-avatar__options { - height: 50px; - width: 100%; - display: flex; - flex: 0 0 50px; - opacity: 1; - justify-content: space-evenly; - } - .contact-header-avatar__options > [class^='icon-'] { - width: 25%; - display: block; - cursor: pointer; - opacity: 0.5; - } - .contact-header-avatar__options > [class^='icon-']:hover { - opacity: 0.8; - } } } -.contact-header-avatar-options { - top: 0; - background-color: rgba(0, 0, 0, 0.2); - display: flex; - width: 100%; - height: 100%; - overflow: hidden; -} - - diff --git a/css/icons.scss b/css/icons.scss index 5f121f01..a543346f 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -64,7 +64,7 @@ @include icon-color('download', 'actions', '#fffffe', 1, true); } -.icon-upload-force-white { +.icon-picture-force-white { // using #fffffe to trick the accessibility dark theme icon invert - @include icon-color('upload', 'actions', '#fffffe', 1, true); + @include icon-color('picture', 'places', '#fffffe', 1, true); } diff --git a/package-lock.json b/package-lock.json index df6b0b70..99635bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2257,7 +2257,7 @@ "dev": true }, "cdav-library": { - "version": "github:nextcloud/cdav-library#56ea1c8f50fecd8cc7c64e4d98ededfa84e6b586", + "version": "github:nextcloud/cdav-library#e8f29188a90b1ef346882ac0c1b2c39f0cf42f84", "from": "github:nextcloud/cdav-library", "requires": { "@babel/polyfill": "^7.4.0" diff --git a/package.json b/package.json index 068dae36..e3f954ec 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "test:watch": "mocha-webpack -w --webpack-config webpack.test.js --interactive false --require tests/setup.js \"tests/js/**/*.spec.js\"" }, "dependencies": { + "axios": "^0.18.0", "cdav-library": "github:nextcloud/cdav-library", "debounce": "^1.2.0", "downloadjs": "^1.4.7", diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 75014703..ac1ff0b1 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -134,8 +134,6 @@ import PQueue from 'p-queue' import qr from 'qr-image' import { stringify } from 'ical.js' -import { Modal } from 'nextcloud-vue' - import rfcProps from 'Models/rfcProps' import validate from 'Services/validate' @@ -157,8 +155,7 @@ export default { PropertyGroups, PropertyRev, AddNewProp, - ContactAvatar, - Modal + ContactAvatar }, props: { diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue index 54636f17..7557cec2 100644 --- a/src/components/ContactDetails/ContactDetailsAvatar.vue +++ b/src/components/ContactDetails/ContactDetailsAvatar.vue @@ -22,25 +22,39 @@ --> <template> - <div :class="{'maximised':maximizeAvatar }" class="contact-header-avatar"> + <div class="contact-header-avatar"> <div class="contact-header-avatar__wrapper"> - <div class="contact-header-avatar__background" @click="toggleSize" /> + <div class="contact-header-avatar__background" @click="toggleModal" /> <div v-if="contact.photo" :style="{ 'backgroundImage': `url(${photo})` }" class="contact-header-avatar__photo" - @click="toggleSize" /> - <div class="contact-header-avatar__options"> - <input id="contact-avatar-upload" type="file" class="hidden" - accept="image/*" @change="processFile"> - <label v-if="!contact.addressbook.readOnly" v-tooltip.auto="t('contacts', 'Upload a new picture')" - for="contact-avatar-upload" class="icon-upload-force-white" @click="processFile" /> - <div v-if="maximizeAvatar && !contact.addressbook.readOnly" class="icon-delete-force-white" @click="removePhoto" /> - <a v-if="maximizeAvatar" :href="contact.url + '?photo'" class="icon-download-force-white" /> + @click="toggleModal" /> + + <div v-click-outside="closeMenu" class="contact-header-avatar__options"> + <a v-tooltip.bottom="t('contacts', 'Add a new picture')" href="#" class="contact-avatar-options" + :class="loading ? 'icon-loading-small' : 'icon-picture-force-white'" + @click.prevent="toggleMenu" /> + <input id="contact-avatar-upload" ref="uploadInput" type="file" + class="hidden" accept="image/*" @change="processFile"> + </div> + + <modal v-if="maximizeAvatar" class="contact-header-modal__photo" :actions="modalActions" + @close="toggleModal"> + <img :src="photo" class="contact-header-modal__photo"> + </modal> + + <!-- out of the avatar__options because of the overflow hidden --> + <div :class="{ 'open': opened }" class="contact-avatar-options__popovermenu popovermenu"> + <popover-menu :menu="actions" /> </div> </div> </div> </template> <script> +import { pickFileOrDirectory } from 'nextcloud-server/dist/files' +import { generateRemoteUrl } from 'nextcloud-server/dist/router' + +const axios = () => import('axios') export default { name: 'ContactAvatar', @@ -51,9 +65,13 @@ export default { required: true } }, + data() { return { - maximizeAvatar: false + maximizeAvatar: false, + opened: false, + loading: false, + root: generateRemoteUrl(`dav/files/${OC.getCurrentUser().uid}`) } }, computed: { @@ -65,6 +83,35 @@ export default { return `data:image;base64,${this.contact.photo.split(',').pop()}` } return this.contact.photo + }, + actions() { + return [ + { + icon: 'icon-upload', + text: t('contacts', 'Upload a new picture'), + action: this.selectFileInput + }, + { + icon: 'icon-picture', + text: t('contacts', 'Choose from files'), + action: this.selectFilePicker + } + ] + }, + modalActions() { + return [...this.actions, ...[ + { + icon: 'icon-delete', + text: t('contacts', 'Delete picture'), + action: this.removePhoto + }, + { + icon: 'icon-download', + text: t('contacts', 'Download picture'), + href: this.contact.url + '?photo', + target: '_blank' + } + ]] } }, methods: { @@ -74,32 +121,47 @@ export default { * @param {Object} event the event object containing the image */ processFile(event) { - if (event.target.files) { + if (event.target.files && !this.loading) { + this.closeMenu() + let file = event.target.files[0] if (file && file.size && file.size <= 1 * 1024 * 1024) { let reader = new FileReader() let self = this - // check if photo property exists to decide whether to add/update it - reader.onload = function(e) { - self.contact.photo - ? self.contact.photo = reader.result - : self.contact.vCard.addPropertyWithValue('photo', reader.result) - self.$store.dispatch('updateContact', self.contact) + reader.onload = function(e) { + self.setPhoto(reader.result) } + reader.readAsDataURL(file) } else { OC.Notification.showTemporary(t('contacts', 'Image is too big (max 1MB).')) // reset input event.target.value = '' + this.loading = false } } }, /** + * Update the contact photo + * + * @param {String} value the photo as base64 + */ + setPhoto(value) { + // check if photo property exists to decide whether to add/update it + this.contact.photo + ? this.contact.photo = value + : this.contact.vCard.addPropertyWithValue('photo', value) + + this.$store.dispatch('updateContact', this.contact) + this.loading = false + }, + + /** * Toggle the full image preview */ - toggleSize() { + toggleModal() { // maximise or minimise avatar photo this.maximizeAvatar = !this.maximizeAvatar }, @@ -111,6 +173,57 @@ export default { this.contact.vCard.removeProperty('photo') this.maximizeAvatar = !this.maximizeAvatar this.$store.dispatch('updateContact', this.contact) + }, + + /** + * Picker handlers + */ + selectFileInput() { + if (!this.loading) { + this.$refs.uploadInput.click() + } + }, + async selectFilePicker() { + if (!this.loading) { + const file = await pickFileOrDirectory( + t('contacts', 'Pick an avatar'), + false, + [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/x-xbitmap', + 'image/bmp', + 'image/svg+xml' + ] + ) + if (file) { + this.loading = true + try { + const { get } = await axios() + const response = await get(`${this.root}${file}`, { + responseType: 'arraybuffer' + }) + const data = `data:${response.headers['content-type']};base64,${Buffer.from(response.data, 'binary').toString('base64')}` + this.setPhoto(data) + } catch (error) { + OC.Notification.showTemporary(t('contacts', 'Error while processing the picture.')) + console.error(error) + this.loading = false + } + } + } + }, + + /** + * Menu handling + */ + toggleMenu() { + // only open if not loading + this.opened = !this.opened ? !this.opened && !this.loading : false + }, + closeMenu() { + this.opened = false } } diff --git a/src/main.js b/src/main.js index bc6e37a3..6b74fd5e 100644 --- a/src/main.js +++ b/src/main.js @@ -27,7 +27,7 @@ import { sync } from 'vuex-router-sync' import { generateFilePath } from 'nextcloud-server/dist/router' /** GLOBAL COMPONENTS AND DIRECTIVE */ -import { Action, DatetimePicker, Multiselect, PopoverMenu } from 'nextcloud-vue' +import { Action, DatetimePicker, Multiselect, PopoverMenu, Modal } from 'nextcloud-vue' import ClickOutside from 'vue-click-outside' import { VTooltip } from 'v-tooltip' import VueClipboard from 'vue-clipboard2' @@ -43,12 +43,16 @@ __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-next-line __webpack_public_path__ = generateFilePath('contacts', '', 'js/') +// Register global components Vue.component('Action', Action) Vue.component('DatetimePicker', DatetimePicker) +Vue.component('Modal', Modal) Vue.component('Multiselect', Multiselect) Vue.component('PopoverMenu', PopoverMenu) + Vue.directive('ClickOutside', ClickOutside) Vue.directive('Tooltip', VTooltip) + Vue.use(VueClipboard) sync(store, router) |