mirror of https://github.com/nextcloud/server.git
Remake profile picture saving with Vue
Signed-off-by: Christopher Ng <chrng8@gmail.com>pull/33409/head
parent
f167fe0ceb
commit
f44d2586b1
File diff suppressed because one or more lines are too long
@ -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>
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue