Skip to content

Commit

Permalink
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 0efe54a commit 5d650ec
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 118 deletions.
217 changes: 99 additions & 118 deletions apps/settings/src/components/PersonalInfo/AvatarSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,70 +26,67 @@
:readable="avatar.readable"
:scope.sync="avatar.scope" />

<div v-if="!cropping" class="avatar__preview">
<Avatar
:user="userId"
:aria-label="t('settings', 'Your profile picture')"
:disabled-menu="true"
:disabled-tooltip="true"
:show-user-status="false"
:size="180"
:key="avatarKey"
/>
<Button
@click="refreshAvatar"
/>

<div v-if="!cropping" class="avatar__container">
<div class="avatar__preview">
<Avatar 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="avatarKey"
/>
<div v-else class="icon-loading"></div>
</div>
<template v-if="avatarChangeSupported">
<div class="avatar__buttons">
<Button :aria-label="t('core', 'Upload profile picture')"
@click="showFileChooser">
@click="chooseLocalImage">
<template #icon>
<Upload :size="20" />
</template>
</Button>
<Button :aria-label="t('core', 'Select from files')"
@click="showFilePickerDialog">
@click="openFilePicker">
<template #icon>
<Folder :size="20" />
</template>
</Button>
<Button :aria-label="t('core', 'Remove profile picture')"
<Button v-if="!isGenerated"
:aria-label="t('core', 'Remove profile picture')"
@click="removeAvatar">
<template #icon>
<Delete :size="20" />
</template>
</Button>
</div>
<p><em>{{ t('core', 'png or jpg, max. 20 MB') }}</em></p>
<span>{{ t('settings', 'png or jpg, max. 20 MB') }}</span>
</template>
<span v-else>
{{ t('settings', 'Picture provided by original account') }}
</span>
</div>

<div v-else class="avatar-crop">
<div class="crop-area">
<VueCropper
ref="cropper"
:aspect-ratio="1 / 1"
:src="imgSrc"
preview=".preview" />
<template v-else>
<VueCropper ref="cropper"
class="avatar__cropper"
:aspect-ratio="1 / 1" />
<div class="avatar__buttons">
<Button @click="cropping = false">
{{ t('settings', 'Cancel') }}
</Button>
<Button type="primary"
@click="saveAvatar">
{{ t('settings', 'Save profile picture') }}
</Button>
</div>
<Button @click="imgSrc = null">
{{ t('core', 'Cancel') }}
</Button>
<Button type="primary"
@click="cropImage">
{{ t('core', 'Set avatar') }}
</Button>
</div>
</template>

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

Expand All @@ -104,6 +101,7 @@ import { getCurrentUser } from '@nextcloud/auth'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import { emit, subscribe } from '@nextcloud/event-bus'
import { showError } from '@nextcloud/dialogs'
import 'cropperjs/dist/cropper.css'

import Upload from 'vue-material-design-icons/Upload'
Expand All @@ -117,10 +115,11 @@ const { avatar } = loadState('settings', 'personalInfoParameters', {})
const { avatarChangeSupported } = loadState('settings', 'accountParameters', {})

const picker = getFilePickerBuilder(t('settings', 'Select profile picture'))
.setMimeTypeFilter(['image/png', 'image/jpeg'])
.setMultiSelect(false)
.setModal(true)
.setType(1)
.allowDirectories()
.allowDirectories(false)
.build()

export default {
Expand All @@ -140,23 +139,23 @@ export default {
return {
avatar: { ...avatar, readable: NAME_READABLE_ENUM[avatar.name] },
avatarChangeSupported,
imgSrc: null,
cropping: false,
loading: false,
imgSrc: null,
userId: getCurrentUser().uid,
displayName: getCurrentUser().displayName,
avatarKey: 'key',
avatarKey: oc_userconfig.avatar.version,
isGenerated: oc_userconfig.avatar.generated,
// tempUrl: generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000),
}
},

created() {
subscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
subscribe('settings:avatar:updated', this.handleAvatarUpdate)
// FIXME refresh all other avatars on the page when updated
},

beforeDestroy() {
unsubscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
unsubscribe('settings:avatar:updated', this.handleAvatarUpdate)
},


Expand All @@ -167,123 +166,105 @@ export default {
},

methods: {
handleDisplayNameUpdate(displayName) {
this.avatarKey = displayName

// 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
oc_userconfig.avatar.version = displayName
},

handleAvatarUpdate(timestamp) {
this.avatarKey = timestamp
},

refreshAvatar() {
this.avatarKey = Math.random().toString(36).substring(2)
oc_userconfig.avatar.version = this.avatarKey
console.log(`avatar key: ${this.avatarKey}`)
},

cropImage() {
this.imgSrc = null
this.saveAvatar()
chooseLocalImage() {
this.$refs.input.click()
},

setImage(e) {
cropImage(e) {
this.loading = true
const file = e.target.files[0]
if (file.type.indexOf('image/') === -1) {
alert('Please select an image file')
if (!file.type.startsWith('image/')) {
showError(t('settings', 'Please select an image file'))
return
}
if (typeof FileReader === 'function') {
const reader = new FileReader()
reader.onload = (event) => {
this.imgSrc = event.target.result
this.$nextTick(() => this.$refs.cropper.replace(event.target.result))
}
reader.readAsDataURL(file)
emit('settings:avatar:updated', Date.now())
// FIXME emit event when avatar image has been updated and refresh all avatars on the page
} else {
alert('Sorry, FileReader API not supported')
}
},

showFileChooser() {
this.$refs.input.click()
const reader = new FileReader()
reader.onload = (e) => {
this.$refs.cropper.replace(e.target.result)
}
reader.readAsDataURL(file)
this.cropping = true
// this.handleAvatarUpdate(false)
},

saveAvatar() {
this.$refs.cropper.getCroppedCanvas().toBlob((blob) => {
this.cropping = false
this.loading = true

this.$refs.cropper.getCroppedCanvas().toBlob(async (blob) => {
const formData = new FormData()
formData.append('files[]', blob)
axios.post(generateUrl('/avatar/'), formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
}).then(() => {
})
await axios.post(generateUrl('/avatar'), formData)
this.loading = false
this.handleAvatarUpdate(false)
})
},

async showFilePickerDialog() {
async openFilePicker() {
const path = await picker.pick()
await axios.post(generateUrl('/avatar/'), { path })
this.imgSrc = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
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)
},

async removeAvatar() {
await axios.delete(generateUrl('/avatar/'))
window.oc_userconfig.avatar.generated = true
await axios.delete(generateUrl('/avatar'))
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
},

handleAvatarUpdate(isGenerated) {
// Update the avatar version so that avatar update handlers refresh correctly
this.avatarKey = oc_userconfig.avatar.version = Date.now()
this.isGenerated = oc_userconfig.avatar.generated = isGenerated
emit('settings:avatar:updated', oc_userconfig.avatar.version)
// FIXME refresh all other avatars on the page when updated
},
},
}
</script>

<style lang="scss" scoped>
input[type="file"] {
display: none;
}

.crop-area, .cropped-image {
width: 300px;
}

.avatar {
&__preview {
&__container {
display: flex;
flex-direction: column;
gap: 16px 0;
align-items: center;
width: 300px;

.cropped-image {
width: 200px;
height: 200px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 12px;
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;
}
}

img {
width: 100%;
}

.crop-placeholder {
width: 300px;
height: 300px;
border-radius: 50%;
background: #ccc;
input[type="file"] {
display: none;
}

::v-deep .cropper-view-box {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export default {
},

onSave(value) {
if (oc_userconfig.avatar.generated) {
// Update the avatar version so that avatar update handlers refresh correctly
oc_userconfig.avatar.version = Date.now()
}
emit('settings:display-name:updated', value)
},
}
Expand Down
16 changes: 16 additions & 0 deletions core/Controller/AvatarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@ public function postAvatar(?string $path = null): JSONResponse {
);
}

if ($image->width() === $image->height()) {
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image);
// Clean up
$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->cache->set('tmpAvatar', $image->data(), 7200);
return new JSONResponse(
['data' => 'notsquare'],
Expand Down

0 comments on commit 5d650ec

Please sign in to comment.