Skip to content

Commit 5d650ec

Browse files
committed
fixup! fixup! Remake profile picture saving with Vue
Signed-off-by: Christopher Ng <chrng8@gmail.com>
1 parent 0efe54a commit 5d650ec

3 files changed

Lines changed: 119 additions & 118 deletions

File tree

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

Lines changed: 99 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -26,70 +26,67 @@
2626
:readable="avatar.readable"
2727
:scope.sync="avatar.scope" />
2828

29-
<div v-if="!cropping" class="avatar__preview">
30-
<Avatar
31-
:user="userId"
32-
:aria-label="t('settings', 'Your profile picture')"
33-
:disabled-menu="true"
34-
:disabled-tooltip="true"
35-
:show-user-status="false"
36-
:size="180"
37-
:key="avatarKey"
38-
/>
39-
<Button
40-
@click="refreshAvatar"
41-
/>
42-
29+
<div v-if="!cropping" class="avatar__container">
30+
<div class="avatar__preview">
31+
<Avatar v-if="!loading"
32+
:user="userId"
33+
:aria-label="t('settings', 'Your profile picture')"
34+
:disabled-menu="true"
35+
:disabled-tooltip="true"
36+
:show-user-status="false"
37+
:size="180"
38+
:key="avatarKey"
39+
/>
40+
<div v-else class="icon-loading"></div>
41+
</div>
4342
<template v-if="avatarChangeSupported">
4443
<div class="avatar__buttons">
4544
<Button :aria-label="t('core', 'Upload profile picture')"
46-
@click="showFileChooser">
45+
@click="chooseLocalImage">
4746
<template #icon>
4847
<Upload :size="20" />
4948
</template>
5049
</Button>
5150
<Button :aria-label="t('core', 'Select from files')"
52-
@click="showFilePickerDialog">
51+
@click="openFilePicker">
5352
<template #icon>
5453
<Folder :size="20" />
5554
</template>
5655
</Button>
57-
<Button :aria-label="t('core', 'Remove profile picture')"
56+
<Button v-if="!isGenerated"
57+
:aria-label="t('core', 'Remove profile picture')"
5858
@click="removeAvatar">
5959
<template #icon>
6060
<Delete :size="20" />
6161
</template>
6262
</Button>
6363
</div>
64-
<p><em>{{ t('core', 'png or jpg, max. 20 MB') }}</em></p>
64+
<span>{{ t('settings', 'png or jpg, max. 20 MB') }}</span>
6565
</template>
6666
<span v-else>
6767
{{ t('settings', 'Picture provided by original account') }}
6868
</span>
6969
</div>
7070

71-
<div v-else class="avatar-crop">
72-
<div class="crop-area">
73-
<VueCropper
74-
ref="cropper"
75-
:aspect-ratio="1 / 1"
76-
:src="imgSrc"
77-
preview=".preview" />
71+
<template v-else>
72+
<VueCropper ref="cropper"
73+
class="avatar__cropper"
74+
:aspect-ratio="1 / 1" />
75+
<div class="avatar__buttons">
76+
<Button @click="cropping = false">
77+
{{ t('settings', 'Cancel') }}
78+
</Button>
79+
<Button type="primary"
80+
@click="saveAvatar">
81+
{{ t('settings', 'Save profile picture') }}
82+
</Button>
7883
</div>
79-
<Button @click="imgSrc = null">
80-
{{ t('core', 'Cancel') }}
81-
</Button>
82-
<Button type="primary"
83-
@click="cropImage">
84-
{{ t('core', 'Set avatar') }}
85-
</Button>
86-
</div>
84+
</template>
8785

8886
<input ref="input"
8987
type="file"
90-
name="image"
9188
accept="image/*"
92-
@change="setImage">
89+
@change="cropImage">
9390
</div>
9491
</template>
9592

@@ -104,6 +101,7 @@ import { getCurrentUser } from '@nextcloud/auth'
104101
import { generateUrl } from '@nextcloud/router'
105102
import { loadState } from '@nextcloud/initial-state'
106103
import { emit, subscribe } from '@nextcloud/event-bus'
104+
import { showError } from '@nextcloud/dialogs'
107105
import 'cropperjs/dist/cropper.css'
108106
109107
import Upload from 'vue-material-design-icons/Upload'
@@ -117,10 +115,11 @@ const { avatar } = loadState('settings', 'personalInfoParameters', {})
117115
const { avatarChangeSupported } = loadState('settings', 'accountParameters', {})
118116
119117
const picker = getFilePickerBuilder(t('settings', 'Select profile picture'))
118+
.setMimeTypeFilter(['image/png', 'image/jpeg'])
120119
.setMultiSelect(false)
121120
.setModal(true)
122121
.setType(1)
123-
.allowDirectories()
122+
.allowDirectories(false)
124123
.build()
125124
126125
export default {
@@ -140,23 +139,23 @@ export default {
140139
return {
141140
avatar: { ...avatar, readable: NAME_READABLE_ENUM[avatar.name] },
142141
avatarChangeSupported,
143-
imgSrc: null,
144142
cropping: false,
143+
loading: false,
144+
imgSrc: null,
145145
userId: getCurrentUser().uid,
146146
displayName: getCurrentUser().displayName,
147-
avatarKey: 'key',
147+
avatarKey: oc_userconfig.avatar.version,
148+
isGenerated: oc_userconfig.avatar.generated,
149+
// tempUrl: generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000),
148150
}
149151
},
150152
151153
created() {
152154
subscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
153-
subscribe('settings:avatar:updated', this.handleAvatarUpdate)
154-
// FIXME refresh all other avatars on the page when updated
155155
},
156156
157157
beforeDestroy() {
158158
unsubscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
159-
unsubscribe('settings:avatar:updated', this.handleAvatarUpdate)
160159
},
161160
162161
@@ -167,123 +166,105 @@ export default {
167166
},
168167
169168
methods: {
170-
handleDisplayNameUpdate(displayName) {
171-
this.avatarKey = displayName
172-
173-
// FIXME update the avatar version only when a refresh is needed
174-
// If displayName based and displayName updated: refresh
175-
// If displayName based and image updated: refresh
176-
// If image and image updated: refresh
177-
// If image and displayName updated: do not refresh
178-
oc_userconfig.avatar.version = displayName
179-
},
180-
181-
handleAvatarUpdate(timestamp) {
182-
this.avatarKey = timestamp
183-
},
184-
185-
refreshAvatar() {
186-
this.avatarKey = Math.random().toString(36).substring(2)
187-
oc_userconfig.avatar.version = this.avatarKey
188-
console.log(`avatar key: ${this.avatarKey}`)
189-
},
190-
191-
cropImage() {
192-
this.imgSrc = null
193-
this.saveAvatar()
169+
chooseLocalImage() {
170+
this.$refs.input.click()
194171
},
195172
196-
setImage(e) {
173+
cropImage(e) {
174+
this.loading = true
197175
const file = e.target.files[0]
198-
if (file.type.indexOf('image/') === -1) {
199-
alert('Please select an image file')
176+
if (!file.type.startsWith('image/')) {
177+
showError(t('settings', 'Please select an image file'))
200178
return
201179
}
202-
if (typeof FileReader === 'function') {
203-
const reader = new FileReader()
204-
reader.onload = (event) => {
205-
this.imgSrc = event.target.result
206-
this.$nextTick(() => this.$refs.cropper.replace(event.target.result))
207-
}
208-
reader.readAsDataURL(file)
209-
emit('settings:avatar:updated', Date.now())
210-
// FIXME emit event when avatar image has been updated and refresh all avatars on the page
211-
} else {
212-
alert('Sorry, FileReader API not supported')
213-
}
214-
},
215180
216-
showFileChooser() {
217-
this.$refs.input.click()
181+
const reader = new FileReader()
182+
reader.onload = (e) => {
183+
this.$refs.cropper.replace(e.target.result)
184+
}
185+
reader.readAsDataURL(file)
186+
this.cropping = true
187+
// this.handleAvatarUpdate(false)
218188
},
219189
220190
saveAvatar() {
221-
this.$refs.cropper.getCroppedCanvas().toBlob((blob) => {
191+
this.cropping = false
192+
this.loading = true
193+
194+
this.$refs.cropper.getCroppedCanvas().toBlob(async (blob) => {
222195
const formData = new FormData()
223196
formData.append('files[]', blob)
224-
axios.post(generateUrl('/avatar/'), formData, {
225-
headers: {
226-
'Content-Type': 'multipart/form-data',
227-
},
228-
}).then(() => {
229-
})
197+
await axios.post(generateUrl('/avatar'), formData)
198+
this.loading = false
199+
this.handleAvatarUpdate(false)
230200
})
231201
},
232202
233-
async showFilePickerDialog() {
203+
async openFilePicker() {
234204
const path = await picker.pick()
235205
await axios.post(generateUrl('/avatar/'), { path })
236-
this.imgSrc = generateUrl('/avatar/tmp') + '?requesttoken=' + encodeURIComponent(OC.requestToken) + '#' + Math.floor(Math.random() * 1000)
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)
237211
},
238212
239213
async removeAvatar() {
240-
await axios.delete(generateUrl('/avatar/'))
241-
window.oc_userconfig.avatar.generated = true
214+
await axios.delete(generateUrl('/avatar'))
215+
this.handleAvatarUpdate(true)
216+
},
217+
218+
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
224+
this.avatarKey = oc_userconfig.avatar.version
225+
},
226+
227+
handleAvatarUpdate(isGenerated) {
228+
// Update the avatar version so that avatar update handlers refresh correctly
229+
this.avatarKey = oc_userconfig.avatar.version = Date.now()
230+
this.isGenerated = oc_userconfig.avatar.generated = isGenerated
231+
emit('settings:avatar:updated', oc_userconfig.avatar.version)
232+
// FIXME refresh all other avatars on the page when updated
242233
},
243234
},
244235
}
245236
</script>
246237

247238
<style lang="scss" scoped>
248-
input[type="file"] {
249-
display: none;
250-
}
251-
252-
.crop-area, .cropped-image {
253-
width: 300px;
254-
}
255-
256239
.avatar {
257-
&__preview {
240+
&__container {
258241
display: flex;
259242
flex-direction: column;
243+
gap: 16px 0;
260244
align-items: center;
261245
width: 300px;
262246
263-
.cropped-image {
264-
width: 200px;
265-
height: 200px;
266-
border-radius: 50%;
267-
overflow: hidden;
268-
margin-bottom: 12px;
247+
span {
248+
color: var(--color-text-lighter);
269249
}
270250
}
271251
252+
&__preview {
253+
display: flex;
254+
justify-content: center;
255+
align-items: center;
256+
width: 180px;
257+
height: 180px;
258+
}
259+
272260
&__buttons {
273261
display: flex;
274262
gap: 0 10px;
275263
}
276264
}
277265
278-
img {
279-
width: 100%;
280-
}
281-
282-
.crop-placeholder {
283-
width: 300px;
284-
height: 300px;
285-
border-radius: 50%;
286-
background: #ccc;
266+
input[type="file"] {
267+
display: none;
287268
}
288269
289270
::v-deep .cropper-view-box {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export default {
5959
},
6060
6161
onSave(value) {
62+
if (oc_userconfig.avatar.generated) {
63+
// Update the avatar version so that avatar update handlers refresh correctly
64+
oc_userconfig.avatar.version = Date.now()
65+
}
6266
emit('settings:display-name:updated', value)
6367
},
6468
}

core/Controller/AvatarController.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,22 @@ public function postAvatar(?string $path = null): JSONResponse {
215215
);
216216
}
217217

218+
if ($image->width() === $image->height()) {
219+
try {
220+
$avatar = $this->avatarManager->getAvatar($this->userId);
221+
$avatar->set($image);
222+
// Clean up
223+
$this->cache->remove('tmpAvatar');
224+
return new JSONResponse(['status' => 'success']);
225+
} 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+
);
231+
}
232+
}
233+
218234
$this->cache->set('tmpAvatar', $image->data(), 7200);
219235
return new JSONResponse(
220236
['data' => 'notsquare'],

0 commit comments

Comments
 (0)