From 13725f040bca346a7b35b832f36f4e86c5da11e4 Mon Sep 17 00:00:00 2001 From: taehoon Date: Thu, 7 Feb 2019 03:05:59 -0500 Subject: Add avatar crop popup --- src/components/image_cropper/image_cropper.js | 88 +++++++++++++++++++++ src/components/image_cropper/image_cropper.vue | 39 ++++++++++ src/components/modal/modal.js | 17 +++++ src/components/modal/modal.vue | 101 +++++++++++++++++++++++++ src/components/settings/settings.vue | 14 ---- src/components/user_settings/user_settings.js | 29 ++----- src/components/user_settings/user_settings.vue | 52 ++++++++++--- src/i18n/en.json | 2 +- 8 files changed, 294 insertions(+), 48 deletions(-) create mode 100644 src/components/image_cropper/image_cropper.js create mode 100644 src/components/image_cropper/image_cropper.vue create mode 100644 src/components/modal/modal.js create mode 100644 src/components/modal/modal.vue (limited to 'src') diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js new file mode 100644 index 00000000..e4bf5ea2 --- /dev/null +++ b/src/components/image_cropper/image_cropper.js @@ -0,0 +1,88 @@ +import Cropper from 'cropperjs' +import Modal from '../modal/modal.vue' +import 'cropperjs/dist/cropper.css' + +const ImageCropper = { + props: { + trigger: { + type: [String, Element], + 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' + }, + title: { + type: String, + default: 'Crop picture' + }, + saveButtonLabel: { + type: String, + default: 'Save' + } + }, + data () { + return { + cropper: undefined, + dataUrl: undefined, + filename: undefined + } + }, + components: { + Modal + }, + methods: { + destroy () { + this.cropper.destroy() + this.$refs.input.value = '' + this.dataUrl = undefined + }, + submit () { + this.$emit('submit', this.cropper, this.filename) + this.destroy() + }, + pickImage () { + this.$refs.input.click() + }, + createCropper () { + this.cropper = new Cropper(this.$refs.img, this.cropperOptions) + } + }, + mounted () { + // listen for click event on trigger + let trigger = typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger) + if (!trigger) { + this.$emit('error', 'No image make trigger found.', 'user') + } else { + trigger.addEventListener('click', this.pickImage) + } + // listen for input file changes + let fileInput = this.$refs.input + fileInput.addEventListener('change', () => { + if (fileInput.files != null && fileInput.files[0] != null) { + let reader = new FileReader() + reader.onload = (e) => { + this.dataUrl = e.target.result + } + reader.readAsDataURL(fileInput.files[0]) + this.filename = fileInput.files[0].name || 'unknown' + this.$emit('changed', fileInput.files[0], reader) + } + }) + } +} + +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..1c52842c --- /dev/null +++ b/src/components/image_cropper/image_cropper.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js new file mode 100644 index 00000000..36cd7f4a --- /dev/null +++ b/src/components/modal/modal.js @@ -0,0 +1,17 @@ +const Modal = { + props: ['show', 'title'], + methods: { + close: function () { + this.$emit('close') + } + }, + mounted: function () { + document.addEventListener('keydown', (e) => { + if (this.show && e.keyCode === 27) { + this.close() + } + }) + } +} + +export default Modal diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue new file mode 100644 index 00000000..2e3cbe75 --- /dev/null +++ b/src/components/modal/modal.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index dfb2e49d..91232382 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..b3d31d67 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' @@ -24,7 +25,6 @@ const UserSettings = { bannerUploading: false, backgroundUploading: false, followListUploading: false, - avatarPreview: null, bannerPreview: null, backgroundPreview: null, avatarUploadError: null, @@ -41,7 +41,8 @@ const UserSettings = { }, components: { StyleSwitcher, - TabSwitcher + TabSwitcher, + ImageCropper }, computed: { user () { @@ -117,31 +118,13 @@ 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 - } + submitAvatar (cropper) { + const img = cropper.getCroppedCanvas({ width: 150, height: 150 }).toDataURL('image/jpeg') this.avatarUploading = true - this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => { + 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 } diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index d2381da2..9fcd3752 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -47,20 +47,20 @@

{{$t('settings.avatar')}}

{{$t('settings.avatar_size_instruction')}}

-

{{$t('settings.current_avatar')}}

- -

{{$t('settings.set_new_avatar')}}

- - -
- +
+
+ +
+ +
+
- - -
+ +
Error: {{ avatarUploadError }}
+

{{$t('settings.profile_banner')}}

@@ -167,6 +167,8 @@ diff --git a/src/i18n/en.json b/src/i18n/en.json index c664fbfa..90132e3e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -112,7 +112,7 @@ "collapse_subject": "Collapse posts with subjects", "composing": "Composing", "confirm_new_password": "Confirm new password", - "current_avatar": "Your current avatar", + "crop_your_new_avatar": "Crop your new avatar", "current_password": "Current password", "current_profile_banner": "Your current profile banner", "data_import_export_tab": "Data Import / Export", -- cgit v1.2.3-70-g09d2 From a001ffecf0c18091d9f9b7b36b795713412c22af Mon Sep 17 00:00:00 2001 From: taehoon Date: Fri, 8 Feb 2019 10:09:48 -0500 Subject: Add back the existing translation string --- src/i18n/en.json | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/i18n/en.json b/src/i18n/en.json index 90132e3e..d44091e6 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -113,6 +113,7 @@ "composing": "Composing", "confirm_new_password": "Confirm new password", "crop_your_new_avatar": "Crop your new avatar", + "current_avatar": "Your current avatar", "current_password": "Current password", "current_profile_banner": "Your current profile banner", "data_import_export_tab": "Data Import / Export", -- cgit v1.2.3-70-g09d2 From 09949fc7eea10e592339fd620d140eb92a18e28b Mon Sep 17 00:00:00 2001 From: taehoon Date: Fri, 8 Feb 2019 11:01:50 -0500 Subject: Crop avatar image using minWidth/minHeight --- src/components/user_settings/user_settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index b3d31d67..8987c691 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -119,7 +119,7 @@ const UserSettings = { reader.readAsDataURL(file) }, submitAvatar (cropper) { - const img = cropper.getCroppedCanvas({ width: 150, height: 150 }).toDataURL('image/jpeg') + const img = cropper.getCroppedCanvas({ minWidth: 150, minHeight: 150 }).toDataURL('image/jpeg') this.avatarUploading = true this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => { if (!user.error) { -- cgit v1.2.3-70-g09d2 From 205e38ffa99df67d77290a50b0d318dcc4729b3e Mon Sep 17 00:00:00 2001 From: taehoon Date: Fri, 8 Feb 2019 11:23:55 -0500 Subject: Remove event listener when modal is destroyed --- src/components/modal/modal.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index 36cd7f4a..963f4bcd 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -3,14 +3,18 @@ const Modal = { methods: { close: function () { this.$emit('close') - } - }, - mounted: function () { - document.addEventListener('keydown', (e) => { + }, + handleKeydown: function (e) { if (this.show && e.keyCode === 27) { this.close() } - }) + } + }, + mounted: function () { + document.addEventListener('keydown', this.handleKeydown) + }, + destroyed: function () { + document.removeEventListener('keydown', this.handleKeydown) } } -- cgit v1.2.3-70-g09d2 From 228e6681e3fd4a16d08d96edb9eea21d64be5600 Mon Sep 17 00:00:00 2001 From: taehoon Date: Fri, 8 Feb 2019 11:42:02 -0500 Subject: Localization of ImageCropper component --- src/components/image_cropper/image_cropper.js | 14 ++++++++++---- src/components/image_cropper/image_cropper.vue | 4 ++-- src/i18n/en.json | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js index e4bf5ea2..86bd2d67 100644 --- a/src/components/image_cropper/image_cropper.js +++ b/src/components/image_cropper/image_cropper.js @@ -26,12 +26,10 @@ const ImageCropper = { default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon' }, title: { - type: String, - default: 'Crop picture' + type: String }, saveButtonLabel: { - type: String, - default: 'Save' + type: String } }, data () { @@ -44,6 +42,14 @@ const ImageCropper = { components: { Modal }, + computed: { + modalTitle () { + return this.title || this.$t('image_cropper.crop_picture') + }, + modalSaveButtonLabel () { + return this.saveButtonLabel || this.$t('image_cropper.save') + } + }, methods: { destroy () { this.cropper.destroy() diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue index 1c52842c..b2367128 100644 --- a/src/components/image_cropper/image_cropper.vue +++ b/src/components/image_cropper/image_cropper.vue @@ -1,13 +1,13 @@