Skip to content

Commit

Permalink
fixup! fixup! fixup! Remake profile picture saving with Vue
Browse files Browse the repository at this point in the history
Signed-off-by: Christopher Ng <chrng8@gmail.com>
  • Loading branch information
Pytal committed Aug 5, 2022
1 parent 18f33af commit cedfb32
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 48 deletions.
106 changes: 63 additions & 43 deletions apps/settings/src/components/PersonalInfo/AvatarSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
-->

<template>
<div>
<section>
<HeaderBar :input-id="inputId"
:readable="avatar.readable"
:scope.sync="avatar.scope" />

<div v-if="!cropping" class="avatar__container">
<div v-show="!cropping" class="avatar__container">
<div class="avatar__preview">
<Avatar v-if="!loading"
:user="userId"
Expand All @@ -35,26 +35,25 @@
:disabled-tooltip="true"
:show-user-status="false"
:size="180"
:key="avatarKey"
/>
<div v-else class="icon-loading"></div>
:key="avatarKey" />
<div v-else class="icon-loading" />
</div>
<template v-if="avatarChangeSupported">
<div class="avatar__buttons">
<Button :aria-label="t('core', 'Upload profile picture')"
@click="chooseLocalImage">
<Button :aria-label="t('settings', 'Upload profile picture')"
@click="activateLocalFilePicker">
<template #icon>
<Upload :size="20" />
</template>
</Button>
<Button :aria-label="t('core', 'Select from files')"
<Button :aria-label="t('settings', 'Select profile picture from files')"
@click="openFilePicker">
<template #icon>
<Folder :size="20" />
</template>
</Button>
<Button v-if="!isGenerated"
:aria-label="t('core', 'Remove profile picture')"
:aria-label="t('settings', 'Remove profile picture')"
@click="removeAvatar">
<template #icon>
<Delete :size="20" />
Expand All @@ -68,40 +67,41 @@
</span>
</div>

<template v-else>
<!-- Use v-show to ensure earlier cropper ref availability -->
<div v-show="cropping">
<VueCropper ref="cropper"
class="avatar__cropper"
:aspect-ratio="1 / 1" />
v-bind="cropperOptions" />
<div class="avatar__buttons">
<Button @click="cropping = false">
<Button @click="cancel">
{{ t('settings', 'Cancel') }}
</Button>
<Button type="primary"
@click="saveAvatar">
{{ t('settings', 'Save profile picture') }}
</Button>
</div>
</template>
</div>

<input ref="input"
type="file"
accept="image/*"
@change="cropImage">
</div>
@change="onChange">
</section>
</template>

<script>
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
import Button from '@nextcloud/vue/dist/Components/Button'
import VueCropper from 'vue-cropperjs'
import axios from '@nextcloud/axios'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { getCurrentUser } from '@nextcloud/auth'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import { generateUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { emit, subscribe } from '@nextcloud/event-bus'
import { showError } from '@nextcloud/dialogs'
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
import Button from '@nextcloud/vue/dist/Components/Button'
import VueCropper from 'vue-cropperjs'
import 'cropperjs/dist/cropper.css'
import Upload from 'vue-material-design-icons/Upload'
Expand All @@ -114,9 +114,9 @@ import { ACCOUNT_PROPERTY_ENUM, NAME_READABLE_ENUM } from '../../constants/Accou
const { avatar } = loadState('settings', 'personalInfoParameters', {})
const { avatarChangeSupported } = loadState('settings', 'accountParameters', {})
const picker = getFilePickerBuilder(t('settings', 'Select profile picture'))
.setMimeTypeFilter(['image/png', 'image/jpeg'])
const picker = getFilePickerBuilder(t('settings', 'Choose your profile picture'))
.setMultiSelect(false)
.setMimeTypeFilter(['image/png', 'image/jpeg'])
.setModal(true)
.setType(1)
.allowDirectories(false)
Expand All @@ -141,12 +141,16 @@ export default {
avatarChangeSupported,
cropping: false,
loading: false,
imgSrc: null,
userId: getCurrentUser().uid,
displayName: getCurrentUser().displayName,
avatarKey: oc_userconfig.avatar.version,
isGenerated: oc_userconfig.avatar.generated,
// tempUrl: generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000),
cropperOptions: {
aspectRatio: 1 / 1,
viewMode: 1,
guides: false,
autoCropArea: 1,
},
}
},
Expand All @@ -166,11 +170,13 @@ export default {
},
methods: {
chooseLocalImage() {
activateLocalFilePicker() {
// Set to null so that selecting the same file will trigger the change event
this.$refs.input.value = null
this.$refs.input.click()
},
cropImage(e) {
onChange(e) {
this.loading = true
const file = e.target.files[0]
if (!file.type.startsWith('image/')) {
Expand All @@ -187,40 +193,54 @@ export default {
// this.handleAvatarUpdate(false)
},
async openFilePicker() {
const path = await picker.pick()
try {
const { data } = await axios.post(generateUrl('/avatar'), { path })
if (data.status === 'success') {
this.handleAvatarUpdate(false)
} else if (data.data === 'notsquare') {
const tempImg = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
this.$refs.cropper.replace(tempImg)
this.cropping = true
}
} catch (e) {
showError(t('settings', 'Error setting profile picture'))
}
},
saveAvatar() {
this.cropping = false
this.loading = true
this.$refs.cropper.getCroppedCanvas().toBlob(async (blob) => {
const formData = new FormData()
formData.append('files[]', blob)
await axios.post(generateUrl('/avatar'), formData)
try {
await axios.post(generateUrl('/avatar'), formData)
} catch (e) {
showError(t('settings', 'Error saving profile picture'))
}
this.loading = false
this.handleAvatarUpdate(false)
})
},
async openFilePicker() {
const path = await picker.pick()
await axios.post(generateUrl('/avatar/'), { path })
this.cropping = true
// TODO crop
// this.$nextTick(() => this.$refs.cropper.replace(event.target.result))
// this.imgSrc = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
this.handleAvatarUpdate(false)
cancel() {
this.cropping = false
this.loading = false
},
async removeAvatar() {
await axios.delete(generateUrl('/avatar'))
try {
await axios.delete(generateUrl('/avatar'))
} catch (e) {
showError(t('settings', 'Error removing profile picture'))
}
this.handleAvatarUpdate(true)
},
handleDisplayNameUpdate() {
// FIXME update the avatar version only when a refresh is needed
// If displayName based and displayName updated: refresh
// If displayName based and image updated: refresh
// If image and image updated: refresh
// If image and displayName updated: do not refresh
this.avatarKey = oc_userconfig.avatar.version
},
Expand Down
7 changes: 2 additions & 5 deletions core/Controller/AvatarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,8 @@ public function postAvatar(?string $path = null): JSONResponse {
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']);
} catch (\Throwable $e) {
$this->logger->log($e, ['app' => 'core']);
return new JSONResponse(
['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]],
Http::STATUS_BAD_REQUEST,
);
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
}
}

Expand Down

0 comments on commit cedfb32

Please sign in to comment.