Merge branch '256-avatar-crop-popup' into 'develop'
Add avatar cropper Closes #256 See merge request pleroma/pleroma-fe!547
This commit is contained in:
commit
7b9a6f8d43
9 changed files with 203 additions and 1124 deletions
|
@ -11,7 +11,7 @@ module.exports = {
|
||||||
'html'
|
'html'
|
||||||
],
|
],
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
'rules': {
|
rules: {
|
||||||
// allow paren-less arrow functions
|
// allow paren-less arrow functions
|
||||||
'arrow-parens': 0,
|
'arrow-parens': 0,
|
||||||
// allow async-await
|
// allow async-await
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"babel-plugin-add-module-exports": "^0.2.1",
|
"babel-plugin-add-module-exports": "^0.2.1",
|
||||||
"babel-plugin-lodash": "^3.2.11",
|
"babel-plugin-lodash": "^3.2.11",
|
||||||
"chromatism": "^3.0.0",
|
"chromatism": "^3.0.0",
|
||||||
|
"cropperjs": "^1.4.3",
|
||||||
"diff": "^3.0.1",
|
"diff": "^3.0.1",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-mocha-reporter": "^2.2.1",
|
||||||
"localforage": "^1.5.0",
|
"localforage": "^1.5.0",
|
||||||
|
|
128
src/components/image_cropper/image_cropper.js
Normal file
128
src/components/image_cropper/image_cropper.js
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
|
||||||
|
const ImageCropper = {
|
||||||
|
props: {
|
||||||
|
trigger: {
|
||||||
|
type: [String, window.Element],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitHandler: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
cropperOptions: {
|
||||||
|
type: Object,
|
||||||
|
default () {
|
||||||
|
return {
|
||||||
|
aspectRatio: 1,
|
||||||
|
autoCropArea: 1,
|
||||||
|
viewMode: 1,
|
||||||
|
movable: false,
|
||||||
|
zoomable: false,
|
||||||
|
guides: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mimes: {
|
||||||
|
type: String,
|
||||||
|
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||||
|
},
|
||||||
|
saveButtonLabel: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
cancelButtonLabel: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
cropper: undefined,
|
||||||
|
dataUrl: undefined,
|
||||||
|
filename: undefined,
|
||||||
|
submitting: false,
|
||||||
|
submitError: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
saveText () {
|
||||||
|
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||||
|
},
|
||||||
|
cancelText () {
|
||||||
|
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||||
|
},
|
||||||
|
submitErrorMsg () {
|
||||||
|
return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
destroy () {
|
||||||
|
if (this.cropper) {
|
||||||
|
this.cropper.destroy()
|
||||||
|
}
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
this.dataUrl = undefined
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
submit () {
|
||||||
|
this.submitting = true
|
||||||
|
this.avatarUploadError = null
|
||||||
|
this.submitHandler(this.cropper, this.filename)
|
||||||
|
.then(() => this.destroy())
|
||||||
|
.catch((err) => {
|
||||||
|
this.submitError = err
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.submitting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pickImage () {
|
||||||
|
this.$refs.input.click()
|
||||||
|
},
|
||||||
|
createCropper () {
|
||||||
|
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
|
||||||
|
},
|
||||||
|
getTriggerDOM () {
|
||||||
|
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||||
|
},
|
||||||
|
readFile () {
|
||||||
|
const fileInput = this.$refs.input
|
||||||
|
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||||
|
let reader = new window.FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.dataUrl = e.target.result
|
||||||
|
this.$emit('open')
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(fileInput.files[0])
|
||||||
|
this.filename = fileInput.files[0].name || 'unknown'
|
||||||
|
this.$emit('changed', fileInput.files[0], reader)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearError () {
|
||||||
|
this.submitError = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
// listen for click event on trigger
|
||||||
|
const trigger = this.getTriggerDOM()
|
||||||
|
if (!trigger) {
|
||||||
|
this.$emit('error', 'No image make trigger found.', 'user')
|
||||||
|
} else {
|
||||||
|
trigger.addEventListener('click', this.pickImage)
|
||||||
|
}
|
||||||
|
// listen for input file changes
|
||||||
|
const fileInput = this.$refs.input
|
||||||
|
fileInput.addEventListener('change', this.readFile)
|
||||||
|
},
|
||||||
|
beforeDestroy: function () {
|
||||||
|
// remove the event listeners
|
||||||
|
const trigger = this.getTriggerDOM()
|
||||||
|
if (trigger) {
|
||||||
|
trigger.removeEventListener('click', this.pickImage)
|
||||||
|
}
|
||||||
|
const fileInput = this.$refs.input
|
||||||
|
fileInput.removeEventListener('change', this.readFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageCropper
|
42
src/components/image_cropper/image_cropper.vue
Normal file
42
src/components/image_cropper/image_cropper.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="image-cropper">
|
||||||
|
<div v-if="dataUrl">
|
||||||
|
<div class="image-cropper-image-container">
|
||||||
|
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
|
||||||
|
</div>
|
||||||
|
<div class="image-cropper-buttons-wrapper">
|
||||||
|
<button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button>
|
||||||
|
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
|
||||||
|
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
|
||||||
|
</div>
|
||||||
|
<div class="alert error" v-if="submitError">
|
||||||
|
{{submitErrorMsg}}
|
||||||
|
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input ref="input" type="file" class="image-cropper-img-input" :accept="mimes">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./image_cropper.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-cropper {
|
||||||
|
&-img-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-image-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-buttons-wrapper {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -311,20 +311,6 @@
|
||||||
color: $fallback--cRed;
|
color: $fallback--cRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.old-avatar {
|
|
||||||
width: 128px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-avatar {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 128px;
|
|
||||||
height: 128px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { unescape } from 'lodash'
|
import { unescape } from 'lodash'
|
||||||
|
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||||
|
import ImageCropper from '../image_cropper/image_cropper.vue'
|
||||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
|
|
||||||
|
@ -20,14 +21,12 @@ const UserSettings = {
|
||||||
followImportError: false,
|
followImportError: false,
|
||||||
followsImported: false,
|
followsImported: false,
|
||||||
enableFollowsExport: true,
|
enableFollowsExport: true,
|
||||||
avatarUploading: false,
|
pickAvatarBtnVisible: true,
|
||||||
bannerUploading: false,
|
bannerUploading: false,
|
||||||
backgroundUploading: false,
|
backgroundUploading: false,
|
||||||
followListUploading: false,
|
followListUploading: false,
|
||||||
avatarPreview: null,
|
|
||||||
bannerPreview: null,
|
bannerPreview: null,
|
||||||
backgroundPreview: null,
|
backgroundPreview: null,
|
||||||
avatarUploadError: null,
|
|
||||||
bannerUploadError: null,
|
bannerUploadError: null,
|
||||||
backgroundUploadError: null,
|
backgroundUploadError: null,
|
||||||
deletingAccount: false,
|
deletingAccount: false,
|
||||||
|
@ -41,7 +40,8 @@ const UserSettings = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StyleSwitcher,
|
StyleSwitcher,
|
||||||
TabSwitcher
|
TabSwitcher,
|
||||||
|
ImageCropper
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
|
@ -117,35 +117,15 @@ const UserSettings = {
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
},
|
},
|
||||||
submitAvatar () {
|
submitAvatar (cropper) {
|
||||||
if (!this.avatarPreview) { return }
|
const img = cropper.getCroppedCanvas().toDataURL('image/jpeg')
|
||||||
|
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
|
||||||
let img = this.avatarPreview
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
let imginfo = new Image()
|
|
||||||
let cropX, cropY, cropW, cropH
|
|
||||||
imginfo.src = img
|
|
||||||
if (imginfo.height > imginfo.width) {
|
|
||||||
cropX = 0
|
|
||||||
cropW = imginfo.width
|
|
||||||
cropY = Math.floor((imginfo.height - imginfo.width) / 2)
|
|
||||||
cropH = imginfo.width
|
|
||||||
} else {
|
|
||||||
cropY = 0
|
|
||||||
cropH = imginfo.height
|
|
||||||
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
|
|
||||||
cropW = imginfo.height
|
|
||||||
}
|
|
||||||
this.avatarUploading = true
|
|
||||||
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
|
|
||||||
if (!user.error) {
|
if (!user.error) {
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
this.avatarPreview = null
|
|
||||||
} else {
|
} else {
|
||||||
this.avatarUploadError = this.$t('upload.error.base') + user.error
|
throw new Error(this.$t('upload.error.base') + user.error)
|
||||||
}
|
}
|
||||||
this.avatarUploading = false
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearUploadError (slot) {
|
clearUploadError (slot) {
|
||||||
|
|
|
@ -48,19 +48,10 @@
|
||||||
<h2>{{$t('settings.avatar')}}</h2>
|
<h2>{{$t('settings.avatar')}}</h2>
|
||||||
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
||||||
<p>{{$t('settings.current_avatar')}}</p>
|
<p>{{$t('settings.current_avatar')}}</p>
|
||||||
<img :src="user.profile_image_url_original" class="old-avatar"></img>
|
<img :src="user.profile_image_url_original" class="current-avatar"></img>
|
||||||
<p>{{$t('settings.set_new_avatar')}}</p>
|
<p>{{$t('settings.set_new_avatar')}}</p>
|
||||||
<img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
|
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
|
||||||
</img>
|
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
|
||||||
<div>
|
|
||||||
<input type="file" @change="uploadFile('avatar', $event)" ></input>
|
|
||||||
</div>
|
|
||||||
<i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
|
|
||||||
<button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
|
|
||||||
<div class='alert error' v-if="avatarUploadError">
|
|
||||||
Error: {{ avatarUploadError }}
|
|
||||||
<i class="button-icon icon-cancel" @click="clearUploadError('avatar')"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.profile_banner')}}</h2>
|
<h2>{{$t('settings.profile_banner')}}</h2>
|
||||||
|
@ -167,6 +158,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.profile-edit {
|
.profile-edit {
|
||||||
.bio {
|
.bio {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -193,5 +186,13 @@
|
||||||
.bg {
|
.bg {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-avatar {
|
||||||
|
display: block;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: $fallback--avatarRadius;
|
||||||
|
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,6 +21,11 @@
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"generic_error": "An error occured"
|
"generic_error": "An error occured"
|
||||||
},
|
},
|
||||||
|
"image_cropper": {
|
||||||
|
"crop_picture": "Crop picture",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "Log in",
|
"login": "Log in",
|
||||||
"description": "Log in with OAuth",
|
"description": "Log in with OAuth",
|
||||||
|
@ -206,6 +211,7 @@
|
||||||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||||
"tooltipRadius": "Tooltips/alerts",
|
"tooltipRadius": "Tooltips/alerts",
|
||||||
|
"upload_a_photo": "Upload a photo",
|
||||||
"user_settings": "User Settings",
|
"user_settings": "User Settings",
|
||||||
"values": {
|
"values": {
|
||||||
"false": "no",
|
"false": "no",
|
||||||
|
|
Loading…
Reference in a new issue