Skip to content

Commit cedfb32

Browse files
committed
fixup! fixup! fixup! Remake profile picture saving with Vue
Signed-off-by: Christopher Ng <[email protected]>
1 parent 18f33af commit cedfb32

File tree

2 files changed

+65
-48
lines changed

2 files changed

+65
-48
lines changed

apps/settings/src/components/PersonalInfo/AvatarSection.vue

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
-->
2222

2323
<template>
24-
<div>
24+
<section>
2525
<HeaderBar :input-id="inputId"
2626
:readable="avatar.readable"
2727
:scope.sync="avatar.scope" />
2828

29-
<div v-if="!cropping" class="avatar__container">
29+
<div v-show="!cropping" class="avatar__container">
3030
<div class="avatar__preview">
3131
<Avatar v-if="!loading"
3232
:user="userId"
@@ -35,26 +35,25 @@
3535
:disabled-tooltip="true"
3636
:show-user-status="false"
3737
:size="180"
38-
:key="avatarKey"
39-
/>
40-
<div v-else class="icon-loading"></div>
38+
:key="avatarKey" />
39+
<div v-else class="icon-loading" />
4140
</div>
4241
<template v-if="avatarChangeSupported">
4342
<div class="avatar__buttons">
44-
<Button :aria-label="t('core', 'Upload profile picture')"
45-
@click="chooseLocalImage">
43+
<Button :aria-label="t('settings', 'Upload profile picture')"
44+
@click="activateLocalFilePicker">
4645
<template #icon>
4746
<Upload :size="20" />
4847
</template>
4948
</Button>
50-
<Button :aria-label="t('core', 'Select from files')"
49+
<Button :aria-label="t('settings', 'Select profile picture from files')"
5150
@click="openFilePicker">
5251
<template #icon>
5352
<Folder :size="20" />
5453
</template>
5554
</Button>
5655
<Button v-if="!isGenerated"
57-
:aria-label="t('core', 'Remove profile picture')"
56+
:aria-label="t('settings', 'Remove profile picture')"
5857
@click="removeAvatar">
5958
<template #icon>
6059
<Delete :size="20" />
@@ -68,40 +67,41 @@
6867
</span>
6968
</div>
7069

71-
<template v-else>
70+
<!-- Use v-show to ensure earlier cropper ref availability -->
71+
<div v-show="cropping">
7272
<VueCropper ref="cropper"
7373
class="avatar__cropper"
74-
:aspect-ratio="1 / 1" />
74+
v-bind="cropperOptions" />
7575
<div class="avatar__buttons">
76-
<Button @click="cropping = false">
76+
<Button @click="cancel">
7777
{{ t('settings', 'Cancel') }}
7878
</Button>
7979
<Button type="primary"
8080
@click="saveAvatar">
8181
{{ t('settings', 'Save profile picture') }}
8282
</Button>
8383
</div>
84-
</template>
84+
</div>
8585

8686
<input ref="input"
8787
type="file"
8888
accept="image/*"
89-
@change="cropImage">
90-
</div>
89+
@change="onChange">
90+
</section>
9191
</template>
9292

9393
<script>
94-
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
95-
import Button from '@nextcloud/vue/dist/Components/Button'
96-
import VueCropper from 'vue-cropperjs'
97-
9894
import axios from '@nextcloud/axios'
99-
import { getFilePickerBuilder } from '@nextcloud/dialogs'
100-
import { getCurrentUser } from '@nextcloud/auth'
101-
import { generateUrl } from '@nextcloud/router'
10295
import { loadState } from '@nextcloud/initial-state'
96+
import { generateUrl } from '@nextcloud/router'
97+
import { getCurrentUser } from '@nextcloud/auth'
98+
import { getFilePickerBuilder } from '@nextcloud/dialogs'
10399
import { emit, subscribe } from '@nextcloud/event-bus'
104100
import { showError } from '@nextcloud/dialogs'
101+
102+
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
103+
import Button from '@nextcloud/vue/dist/Components/Button'
104+
import VueCropper from 'vue-cropperjs'
105105
import 'cropperjs/dist/cropper.css'
106106
107107
import Upload from 'vue-material-design-icons/Upload'
@@ -114,9 +114,9 @@ import { ACCOUNT_PROPERTY_ENUM, NAME_READABLE_ENUM } from '../../constants/Accou
114114
const { avatar } = loadState('settings', 'personalInfoParameters', {})
115115
const { avatarChangeSupported } = loadState('settings', 'accountParameters', {})
116116
117-
const picker = getFilePickerBuilder(t('settings', 'Select profile picture'))
118-
.setMimeTypeFilter(['image/png', 'image/jpeg'])
117+
const picker = getFilePickerBuilder(t('settings', 'Choose your profile picture'))
119118
.setMultiSelect(false)
119+
.setMimeTypeFilter(['image/png', 'image/jpeg'])
120120
.setModal(true)
121121
.setType(1)
122122
.allowDirectories(false)
@@ -141,12 +141,16 @@ export default {
141141
avatarChangeSupported,
142142
cropping: false,
143143
loading: false,
144-
imgSrc: null,
145144
userId: getCurrentUser().uid,
146145
displayName: getCurrentUser().displayName,
147146
avatarKey: oc_userconfig.avatar.version,
148147
isGenerated: oc_userconfig.avatar.generated,
149-
// tempUrl: generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000),
148+
cropperOptions: {
149+
aspectRatio: 1 / 1,
150+
viewMode: 1,
151+
guides: false,
152+
autoCropArea: 1,
153+
},
150154
}
151155
},
152156
@@ -166,11 +170,13 @@ export default {
166170
},
167171
168172
methods: {
169-
chooseLocalImage() {
173+
activateLocalFilePicker() {
174+
// Set to null so that selecting the same file will trigger the change event
175+
this.$refs.input.value = null
170176
this.$refs.input.click()
171177
},
172178
173-
cropImage(e) {
179+
onChange(e) {
174180
this.loading = true
175181
const file = e.target.files[0]
176182
if (!file.type.startsWith('image/')) {
@@ -187,40 +193,54 @@ export default {
187193
// this.handleAvatarUpdate(false)
188194
},
189195
196+
async openFilePicker() {
197+
const path = await picker.pick()
198+
try {
199+
const { data } = await axios.post(generateUrl('/avatar'), { path })
200+
if (data.status === 'success') {
201+
this.handleAvatarUpdate(false)
202+
} else if (data.data === 'notsquare') {
203+
const tempImg = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
204+
this.$refs.cropper.replace(tempImg)
205+
this.cropping = true
206+
}
207+
} catch (e) {
208+
showError(t('settings', 'Error setting profile picture'))
209+
}
210+
},
211+
190212
saveAvatar() {
191213
this.cropping = false
192214
this.loading = true
193215
194216
this.$refs.cropper.getCroppedCanvas().toBlob(async (blob) => {
195217
const formData = new FormData()
196218
formData.append('files[]', blob)
197-
await axios.post(generateUrl('/avatar'), formData)
219+
try {
220+
await axios.post(generateUrl('/avatar'), formData)
221+
} catch (e) {
222+
showError(t('settings', 'Error saving profile picture'))
223+
}
198224
this.loading = false
199225
this.handleAvatarUpdate(false)
200226
})
201227
},
202228
203-
async openFilePicker() {
204-
const path = await picker.pick()
205-
await axios.post(generateUrl('/avatar/'), { path })
206-
this.cropping = true
207-
// TODO crop
208-
// this.$nextTick(() => this.$refs.cropper.replace(event.target.result))
209-
// this.imgSrc = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
210-
this.handleAvatarUpdate(false)
229+
cancel() {
230+
this.cropping = false
231+
this.loading = false
211232
},
212233
213234
async removeAvatar() {
214-
await axios.delete(generateUrl('/avatar'))
235+
try {
236+
await axios.delete(generateUrl('/avatar'))
237+
} catch (e) {
238+
showError(t('settings', 'Error removing profile picture'))
239+
}
215240
this.handleAvatarUpdate(true)
216241
},
217242
218243
handleDisplayNameUpdate() {
219-
// FIXME update the avatar version only when a refresh is needed
220-
// If displayName based and displayName updated: refresh
221-
// If displayName based and image updated: refresh
222-
// If image and image updated: refresh
223-
// If image and displayName updated: do not refresh
224244
this.avatarKey = oc_userconfig.avatar.version
225245
},
226246

core/Controller/AvatarController.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,8 @@ public function postAvatar(?string $path = null): JSONResponse {
223223
$this->cache->remove('tmpAvatar');
224224
return new JSONResponse(['status' => 'success']);
225225
} catch (\Throwable $e) {
226-
$this->logger->log($e, ['app' => 'core']);
227-
return new JSONResponse(
228-
['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]],
229-
Http::STATUS_BAD_REQUEST,
230-
);
226+
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
227+
return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
231228
}
232229
}
233230

0 commit comments

Comments
 (0)