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

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components/PersonalInfo/AvatarSection.vue')
-rw-r--r--apps/settings/src/components/PersonalInfo/AvatarSection.vue333
1 files changed, 333 insertions, 0 deletions
diff --git a/apps/settings/src/components/PersonalInfo/AvatarSection.vue b/apps/settings/src/components/PersonalInfo/AvatarSection.vue
new file mode 100644
index 00000000000..f0ad1b68d3b
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/AvatarSection.vue
@@ -0,0 +1,333 @@
+<!--
+ - @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+-->
+
+<template>
+ <section>
+ <HeaderBar :input-id="avatarChangeSupported ? inputId : null"
+ :readable="avatar.readable"
+ :scope.sync="avatar.scope" />
+
+ <div v-if="!showCropper" class="avatar__container">
+ <div class="avatar__preview">
+ <NcAvatar v-if="!loading"
+ :user="userId"
+ :aria-label="t('settings', 'Your profile picture')"
+ :disabled-menu="true"
+ :disabled-tooltip="true"
+ :show-user-status="false"
+ :size="180"
+ :key="version" />
+ <div v-else class="icon-loading" />
+ </div>
+ <template v-if="avatarChangeSupported">
+ <div class="avatar__buttons">
+ <NcButton :aria-label="t('settings', 'Upload profile picture')"
+ @click="activateLocalFilePicker">
+ <template #icon>
+ <Upload :size="20" />
+ </template>
+ </NcButton>
+ <NcButton :aria-label="t('settings', 'Choose profile picture from files')"
+ @click="openFilePicker">
+ <template #icon>
+ <Folder :size="20" />
+ </template>
+ </NcButton>
+ <NcButton v-if="!isGenerated"
+ :aria-label="t('settings', 'Remove profile picture')"
+ @click="removeAvatar">
+ <template #icon>
+ <Delete :size="20" />
+ </template>
+ </NcButton>
+ </div>
+ <span>{{ t('settings', 'png or jpg, max. 20 MB') }}</span>
+ <input ref="input"
+ :id="inputId"
+ type="file"
+ :accept="validMimeTypes.join(',')"
+ @change="onChange">
+ </template>
+ <span v-else>
+ {{ t('settings', 'Picture provided by original account') }}
+ </span>
+ </div>
+
+ <!-- Use v-show to ensure early cropper ref availability -->
+ <div v-show="showCropper" class="avatar__container">
+ <VueCropper ref="cropper"
+ class="avatar__cropper"
+ v-bind="cropperOptions" />
+ <div class="avatar__cropper-buttons">
+ <NcButton @click="cancel">
+ {{ t('settings', 'Cancel') }}
+ </NcButton>
+ <NcButton type="primary"
+ @click="saveAvatar">
+ {{ t('settings', 'Set as profile picture') }}
+ </NcButton>
+ </div>
+ <span>{{ t('settings', 'Please note that it can take up to 24 hours for your profile picture to be updated everywhere.') }}</span>
+ </div>
+ </section>
+</template>
+
+<script>
+import axios from '@nextcloud/axios'
+import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
+import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
+
+import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton'
+import VueCropper from 'vue-cropperjs'
+// eslint-disable-next-line node/no-extraneous-import
+import 'cropperjs/dist/cropper.css'
+
+import Upload from 'vue-material-design-icons/Upload'
+import Folder from 'vue-material-design-icons/Folder'
+import Delete from 'vue-material-design-icons/Delete'
+
+import HeaderBar from './shared/HeaderBar.vue'
+import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
+
+const { avatar } = loadState('settings', 'personalInfoParameters', {})
+const { avatarChangeSupported } = loadState('settings', 'accountParameters', {})
+
+const VALID_MIME_TYPES = ['image/png', 'image/jpeg']
+
+const picker = getFilePickerBuilder(t('settings', 'Choose your profile picture'))
+ .setMultiSelect(false)
+ .setMimeTypeFilter(VALID_MIME_TYPES)
+ .setModal(true)
+ .setType(1)
+ .allowDirectories(false)
+ .build()
+
+export default {
+ name: 'AvatarSection',
+
+ components: {
+ Delete,
+ Folder,
+ HeaderBar,
+ NcAvatar,
+ NcButton,
+ Upload,
+ VueCropper,
+ },
+
+ data() {
+ return {
+ avatar: { ...avatar, readable: NAME_READABLE_ENUM[avatar.name] },
+ avatarChangeSupported,
+ showCropper: false,
+ loading: false,
+ userId: getCurrentUser().uid,
+ displayName: getCurrentUser().displayName,
+ version: oc_userconfig.avatar.version,
+ isGenerated: oc_userconfig.avatar.generated,
+ validMimeTypes: VALID_MIME_TYPES,
+ cropperOptions: {
+ aspectRatio: 1 / 1,
+ viewMode: 1,
+ guides: false,
+ center: false,
+ highlight: false,
+ autoCropArea: 1,
+ minContainerWidth: 300,
+ minContainerHeight: 300,
+ },
+ }
+ },
+
+ created() {
+ subscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
+ },
+
+ beforeDestroy() {
+ unsubscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
+ },
+
+ computed: {
+ inputId() {
+ return `account-property-${this.avatar.name}`
+ },
+ },
+
+ methods: {
+ activateLocalFilePicker() {
+ // Set to null so that selecting the same file will trigger the change event
+ this.$refs.input.value = null
+ this.$refs.input.click()
+ },
+
+ onChange(e) {
+ this.loading = true
+ const file = e.target.files[0]
+ if (!this.validMimeTypes.includes(file.type)) {
+ showError(t('settings', 'Please select a valid png or jpg file'))
+ this.cancel()
+ return
+ }
+
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ this.$refs.cropper.replace(e.target.result)
+ this.showCropper = true
+ }
+ reader.readAsDataURL(file)
+ },
+
+ async openFilePicker() {
+ const path = await picker.pick()
+ this.loading = true
+ try {
+ const { data } = await axios.post(generateUrl('/avatar'), { path })
+ if (data.status === 'success') {
+ this.handleAvatarUpdate(false)
+ } else if (data.data === 'notsquare') {
+ const tempAvatar = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
+ this.$refs.cropper.replace(tempAvatar)
+ this.showCropper = true
+ } else {
+ showError(data.data.message)
+ this.cancel()
+ }
+ } catch (e) {
+ showError(t('settings', 'Error setting profile picture'))
+ this.cancel()
+ }
+ },
+
+ saveAvatar() {
+ this.showCropper = false
+ this.loading = true
+
+ this.$refs.cropper.getCroppedCanvas().toBlob(async (blob) => {
+ if (blob === null) {
+ showError(t('settings', 'Error cropping profile picture'))
+ this.cancel()
+ return
+ }
+
+ const formData = new FormData()
+ formData.append('files[]', blob)
+ try {
+ await axios.post(generateUrl('/avatar'), formData)
+ this.handleAvatarUpdate(false)
+ } catch (e) {
+ showError(t('settings', 'Error saving profile picture'))
+ this.handleAvatarUpdate(this.isGenerated)
+ }
+ })
+ },
+
+ async removeAvatar() {
+ this.loading = true
+ try {
+ await axios.delete(generateUrl('/avatar'))
+ this.handleAvatarUpdate(true)
+ } catch (e) {
+ showError(t('settings', 'Error removing profile picture'))
+ this.handleAvatarUpdate(this.isGenerated)
+ }
+ },
+
+ cancel() {
+ this.showCropper = false
+ this.loading = false
+ },
+
+ handleAvatarUpdate(isGenerated) {
+ // Update the avatar version so that avatar update handlers refresh correctly
+ this.version = oc_userconfig.avatar.version = Date.now()
+ this.isGenerated = oc_userconfig.avatar.generated = isGenerated
+ this.loading = false
+ emit('settings:avatar:updated', oc_userconfig.avatar.version)
+ /**
+ * FIXME refresh all other avatars on the page when updated,
+ * the NcAvatar component itself should listen to the
+ * global events and optionally live refresh with a prop toggle
+ * https://github.com/nextcloud/nextcloud-vue/issues/2975
+ */
+ },
+
+ handleDisplayNameUpdate() {
+ this.version = oc_userconfig.avatar.version
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.avatar {
+ &__container {
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 16px 0;
+ width: 300px;
+
+ span {
+ color: var(--color-text-lighter);
+ }
+ }
+
+ &__preview {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 180px;
+ height: 180px;
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 0 10px;
+ }
+
+ &__cropper {
+ width: 300px;
+ height: 300px;
+ overflow: hidden;
+
+ &-buttons {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &::v-deep .cropper-view-box {
+ border-radius: 50%;
+ }
+ }
+}
+
+input[type="file"] {
+ display: none;
+}
+</style>