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'
104101import { generateUrl } from ' @nextcloud/router'
105102import { loadState } from ' @nextcloud/initial-state'
106103import { emit , subscribe } from ' @nextcloud/event-bus'
104+ import { showError } from ' @nextcloud/dialogs'
107105import ' cropperjs/dist/cropper.css'
108106
109107import Upload from ' vue-material-design-icons/Upload'
@@ -117,10 +115,11 @@ const { avatar } = loadState('settings', 'personalInfoParameters', {})
117115const { avatarChangeSupported } = loadState (' settings' , ' accountParameters' , {})
118116
119117const 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
126125export 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 {
0 commit comments