aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/image_cropper/image_cropper.js128
-rw-r--r--src/components/image_cropper/image_cropper.vue42
-rw-r--r--src/components/settings/settings.vue14
-rw-r--r--src/components/user_settings/user_settings.js36
-rw-r--r--src/components/user_settings/user_settings.vue25
-rw-r--r--src/i18n/en.json6
6 files changed, 197 insertions, 54 deletions
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
new file mode 100644
index 00000000..990c0370
--- /dev/null
+++ b/src/components/image_cropper/image_cropper.js
@@ -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
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
new file mode 100644
index 00000000..24a6f3bd
--- /dev/null
+++ b/src/components/image_cropper/image_cropper.vue
@@ -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>
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index e5f8fefb..f5e00995 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -311,20 +311,6 @@
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 {
min-height: 28px;
min-width: 10em;
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index d20bf308..dce3eeed 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,6 +1,7 @@
import { unescape } from 'lodash'
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 fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
@@ -20,14 +21,12 @@ const UserSettings = {
followImportError: false,
followsImported: false,
enableFollowsExport: true,
- avatarUploading: false,
+ pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
followListUploading: false,
- avatarPreview: null,
bannerPreview: null,
backgroundPreview: null,
- avatarUploadError: null,
bannerUploadError: null,
backgroundUploadError: null,
deletingAccount: false,
@@ -41,7 +40,8 @@ const UserSettings = {
},
components: {
StyleSwitcher,
- TabSwitcher
+ TabSwitcher,
+ ImageCropper
},
computed: {
user () {
@@ -117,35 +117,15 @@ const UserSettings = {
}
reader.readAsDataURL(file)
},
- submitAvatar () {
- if (!this.avatarPreview) { return }
-
- 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) => {
+ submitAvatar (cropper) {
+ const img = cropper.getCroppedCanvas().toDataURL('image/jpeg')
+ return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
- this.avatarPreview = null
} 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) {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index d2381da2..8ab92e95 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -48,19 +48,10 @@
<h2>{{$t('settings.avatar')}}</h2>
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</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>
- <img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
- </img>
- <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>
+ <button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
+ <image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2>
@@ -167,6 +158,8 @@
</script>
<style lang="scss">
+@import '../../_variables.scss';
+
.profile-edit {
.bio {
margin: 0;
@@ -193,5 +186,13 @@
.bg {
max-width: 100%;
}
+
+ .current-avatar {
+ display: block;
+ width: 150px;
+ height: 150px;
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ }
}
</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index eba90b50..2067e6db 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -21,6 +21,11 @@
"more": "More",
"generic_error": "An error occured"
},
+ "image_cropper": {
+ "crop_picture": "Crop picture",
+ "save": "Save",
+ "cancel": "Cancel"
+ },
"login": {
"login": "Log in",
"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_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",
+ "upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
"false": "no",