summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--css/ContactDetailsAvatar.scss89
-rw-r--r--css/icons.scss4
-rw-r--r--package-lock.json2
-rw-r--r--package.json1
-rw-r--r--src/components/ContactDetails.vue5
-rw-r--r--src/components/ContactDetails/ContactDetailsAvatar.vue151
-rw-r--r--src/main.js6
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)