diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 81100e48038..ab7ba022379 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1065,11 +1065,12 @@ class Binary(http.Controller): # other kwargs are ignored on purpose return self._content_image(xmlid=xmlid, model=model, id=id, field=field, filename_field=filename_field, unique=unique, filename=filename, mimetype=mimetype, - download=download, width=width, height=height, crop=crop, access_token=access_token) + download=download, width=width, height=height, crop=crop, + quality=int(kwargs.get('quality', 0)), access_token=access_token) def _content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas', filename_field='datas_fname', unique=None, filename=None, mimetype=None, - download=None, width=0, height=0, crop=False, access_token=None, + download=None, width=0, height=0, crop=False, quality=0, access_token=None, placeholder='placeholder.png', **kwargs): status, headers, image_base64 = request.env['ir.http'].binary_content( xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, @@ -1083,7 +1084,7 @@ class Binary(http.Controller): if not (width or height): width, height = odoo.tools.image_guess_size_from_field_name(field) - image_base64 = image_process(image_base64, size=(int(width), int(height)), crop=crop) + image_base64 = image_process(image_base64, size=(int(width), int(height)), crop=crop, quality=int(quality)) content = base64.b64decode(image_base64) headers = http.set_safe_image_headers(headers, content) diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 85797ba2e23..bf169fac138 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -153,12 +153,11 @@ class Web_Editor(http.Controller): return True @http.route('/web_editor/attachment/add_data', type='json', auth='user', methods=['POST'], website=True) - def add_data(self, name, data, disable_optimization=False, res_id=False, res_model='ir.ui.view', filters=False, **kwargs): - if not disable_optimization: - try: - data = tools.image_process(data, verify_resolution=True) - except OSError: - pass # not an image + def add_data(self, name, data, quality=0, width=0, height=0, res_id=False, res_model='ir.ui.view', filters=False, **kwargs): + try: + data = tools.image_process(data, size=(width, height), quality=quality, verify_resolution=True) + except OSError: + pass # not an image attachment = self._attachment_create(name=name, data=data, res_id=res_id, res_model=res_model, filters=filters) return attachment._get_media_info() @@ -167,6 +166,22 @@ class Web_Editor(http.Controller): attachment = self._attachment_create(url=url, res_id=res_id, res_model=res_model, filters=filters) return attachment._get_media_info() + @http.route('/web_editor/attachment//update', type='json', auth='user', methods=['POST'], website=True) + def attachment_update(self, attachment, name=None, width=0, height=0, quality=0, copy=False, **kwargs): + if attachment.type == 'url': + raise UserError(_("You cannot change the quality, the width or the name of an URL attachment.")) + if copy: + attachment = attachment.copy() + data = {} + if name: + data['name'] = name + try: + data['datas'] = tools.image_process(attachment.datas, size=(width, height), quality=quality) + except OSError: + pass # not an image + attachment.write(data) + return attachment._get_media_info() + @http.route('/web_editor/attachment/remove', type='json', auth='user', website=True) def remove(self, ids, **kwargs): """ Removes a web-based image attachment if it is used by no view (template) diff --git a/addons/web_editor/models/ir_attachment.py b/addons/web_editor/models/ir_attachment.py index 83f8766b42e..ac441d821e8 100644 --- a/addons/web_editor/models/ir_attachment.py +++ b/addons/web_editor/models/ir_attachment.py @@ -3,7 +3,7 @@ from werkzeug import url_quote -from odoo import api, models, fields +from odoo import api, models, fields, tools class IrAttachment(models.Model): @@ -12,6 +12,8 @@ class IrAttachment(models.Model): local_url = fields.Char("Attachment URL", compute='_compute_local_url') image_src = fields.Char(compute='_compute_image_src') + image_width = fields.Integer(compute='_compute_image_size') + image_height = fields.Integer(compute='_compute_image_size') @api.one def _compute_local_url(self): @@ -32,8 +34,20 @@ class IrAttachment(models.Model): url_quote(attachment.name or ''), ) + @api.multi + @api.depends('datas') + def _compute_image_size(self): + for attachment in self: + try: + image = tools.base64_to_image(attachment.datas) + attachment.image_width = image.width + attachment.image_height = image.height + except Exception: + attachment.image_width = 0 + attachment.image_height = 0 + @api.multi def _get_media_info(self): """Return a dict with the values that we need on the media dialog.""" self.ensure_one() - return self.read(['id', 'name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src'])[0] + return self.read(['id', 'name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height'])[0] diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js index 9ba83486e25..8ff6a100627 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js @@ -61,7 +61,14 @@ var MediaPlugin = AbstractPlugin.extend({ media = $mediaParent[0]; $mediaParent = $mediaParent.parent(); } + + // If the image is for a field the model should handle the resize so we + // use an arbitrary high size of 1920 instead of using the width of the + // the element. + var mediaWidth = $(media).parent().data('oeField') ? 1920 : $(media).width(); + var mediaDialog = new weWidgets.MediaDialog(this.options.parent, { + mediaWidth: mediaWidth, onlyImages: $mediaParent.data('oeField') === 'image' || $mediaParent.data('oeType') === 'image', }, $(media).clone()[0] diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/image_optimize_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/image_optimize_dialog.js new file mode 100644 index 00000000000..4ba7fbf4350 --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/image_optimize_dialog.js @@ -0,0 +1,310 @@ +odoo.define('wysiwyg.widgets.image_optimize_dialog', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); + +var _t = core._t; + +var ImageOptimizeDialog = Dialog.extend({ + template: 'wysiwyg.widgets.image.optimize', + xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat([ + '/web_editor/static/src/xml/wysiwyg.xml', + ]), + events: _.extend({}, Dialog.prototype.events, { + 'click .o_we_width_preset': '_onWidthPresetClick', + 'input #o_we_height': '_onHeightInput', + 'input #o_we_name_input': '_onNameInput', + 'input #o_we_optimize_quality': '_onOptimizeQualityInput', + 'input #o_we_quality_input': '_onQualityInput', + 'input #o_we_width': '_onWidthInput', + 'input .o_we_quality_range': '_onQualityRangeInput', + }), + + /** + * @constructor + */ + init: function (parent, params, options) { + this._super(parent, _.extend({}, { + title: _t("Improve your Image"), + size: 'large', + buttons: [ + {text: _t("Optimize"), classes: 'btn-primary o_we_save', close: false, click: this._onOptimizeClick.bind(this)}, + {text: _t("Keep Original"), close: false, click: this._onKeepOriginalClick.bind(this)} + ], + }, options)); + + this.isExisting = params.isExisting; + this.attachment = params.attachment; + // We do not support resizing and quality for: + // - SVG because it doesn't make sense + // - GIF because our current code is not made to handle all the frames + this.disableResize = ['image/jpeg', 'image/jpe', 'image/jpg', 'image/png'].indexOf(this.attachment.mimetype) === -1; + this.disableQuality = this.disableResize; + this.toggleQuality = this.attachment.mimetype === 'image/png'; + this.optimizedWidth = Math.min(params.optimizedWidth || this.attachment.image_width, this.attachment.image_width); + this.defaultQuality = this.isExisting ? 100 : 80; + this.defaultWidth = parseInt(this.isExisting ? this.attachment.image_width : this.optimizedWidth); + this.defaultHeight = parseInt(this.isExisting || !this.attachment.image_width ? this.attachment.image_height : + this.optimizedWidth / this.attachment.image_width * this.attachment.image_height); + + this.suggestedWidths = []; + this._addSuggestedWidth(128, '128'); + this._addSuggestedWidth(256, '256'); + this._addSuggestedWidth(512, '512'); + this._addSuggestedWidth(1024, '1024'); + this._addSuggestedWidth(this.optimizedWidth, + _.str.sprintf(_t("%d (Suggested)"), this.optimizedWidth)); + this.suggestedWidths.push({ + 'width': this.attachment.image_width, + 'text': _.str.sprintf(_t("%d (Original)"), this.attachment.image_width), + }); + this.suggestedWidths = _.sortBy(this.suggestedWidths, 'width'); + this._updatePreview = _.debounce(this._updatePreview.bind(this), 300); + }, + /** + * @override + */ + start: function () { + var self = this; + var promise = this._super.apply(this, arguments); + this.$nameInput = this.$('#o_we_name_input'); + this.$previewImage = this.$('.o_we_preview_image'); + this.$saveButton = this.$footer.find('.o_we_save'); + + // The following selectors might not contain anything: + // - depending on disableResize + this.$widthPresets = this.$('.o_we_width_preset'); + this.$widthInput = this.$('#o_we_width'); + this.$heightInput = this.$('#o_we_height'); + // - depending on disableQuality + this.$qualityRange = this.$('.o_we_quality_range'); + this.$qualityInput = this.$('#o_we_quality_input'); + // - depending on toggleQuality + this.$qualityCheckBox = this.$('#o_we_optimize_quality'); + + this._updatePreview(); + this._validateForm(); + this.opened().then(function () { + self.$nameInput.focus(); + }); + return promise; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Adds a size to the list of suggested width, only if it is smaller than + * the image width (we don't scale up images). + * + * @private + * @param {number} size: integer + * @param {string} text + */ + _addSuggestedWidth: function (size, text) { + if (size < this.attachment.image_width) { + this.suggestedWidths.push({ + width: size, + text: text, + }); + } + }, + /** + * Gets the current quality from the input, adapts it to match what is + * expected by image tools, and returns it. + * + * The adaptation consists of transforming the quality 100 to 0 to disable + * optimizations, and to convert values between 95 and 99 to 95 because + * values above 95 results in large files with hardly any gain in image + * quality. + * + * See `quality` parameter at: + * https://pillow.readthedocs.io/en/4.0.x/handbook/image-file-formats.html#jpeg + * + * @private + * @returns {number} the quality as integer + */ + _getAdaptedQuality: function () { + var quality = 0; + if (this.$qualityCheckBox.length) { + if (this.$qualityCheckBox.is(":checked")) { + // any value will do, as long as it is not 100 or falsy + quality = 50; + } + } else if (this.$qualityRange.length) { + quality = parseInt(this.$qualityRange.val() || 0); + } + if (quality === 100) { + quality = 0; + } + return Math.min(95, quality); + }, + /** + * Updates the preview to match the current settings, and also highlights + * the width preset buttons if applicable. + * + * @private + */ + _updatePreview: function () { + if (!this._validatePreview()) { + return; + } + var width = parseInt(this.$widthInput.val() || 0); + var height = parseInt(this.$heightInput.val() || 0); + this.$previewImage.attr('src', _.str.sprintf('/web/image/%d/%dx%d?quality=%d', + this.attachment.id, width, height, this._getAdaptedQuality())); + this.$widthPresets.removeClass('active'); + _.each(this.$widthPresets, function (button) { + var $button = $(button); + if (parseInt($button.data('width')) === width) { + $button.addClass('active'); + } + }); + }, + /** + * Validates the form and toggles the save button accordingly. + * + * @private + * @returns {boolean} whether the form is valid + */ + _validateForm: function () { + var name = this.$nameInput.val(); + var isValid = name && this._validatePreview(); + this.$saveButton.prop('disabled', !isValid); + return isValid; + }, + /** + * Validates the preview values. + * + * @private + * @returns {boolean} whether the preview values are valid + */ + _validatePreview: function () { + var isValid = true; + var quality = this._getAdaptedQuality(); + var width = parseInt(this.$widthInput.val() || 0); + var height = parseInt(this.$heightInput.val() || 0); + if (quality < 0 || quality > 100) { + isValid = false; + } + if (width < 0 || width > this.attachment.image_width) { + isValid = false; + } + if (height < 0 || height > this.attachment.image_height) { + isValid = false; + } + return isValid; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onHeightInput: function () { + var height = parseInt(this.$heightInput.val()) || 0; + this.$widthInput + .val(parseInt(height / this.attachment.image_height * this.attachment.image_width)); + this._updatePreview(); + this._validateForm(); + }, + /** + * @private + */ + _onKeepOriginalClick: function () { + this.trigger_up('attachment_updated', this.attachment); + this.close(); + }, + /** + * @private + */ + _onNameInput: function () { + this._validateForm(); + }, + /** + * Handles clicking on the save button: updates the given attachment with + * the selected settings and triggers up the `attachment_updated`` event, + * then closes the dialog. + * + * @private + * @returns {Promise} + */ + _onOptimizeClick: function () { + var self = this; + var name = this.$nameInput.val(); + var params = { + 'name': name, + 'quality': this._getAdaptedQuality(), + 'width': parseInt(this.$widthInput.val() || 0), + 'height': parseInt(this.$heightInput.val() || 0), + }; + if (this.isExisting) { + params['copy'] = true; + } + return this._rpc({ + route: _.str.sprintf('/web_editor/attachment/%d/update', this.attachment.id), + params: params, + }).then(function (attachment) { + self.trigger_up('attachment_updated', attachment); + self.close(); + }); + }, + /** + * @private + */ + _onQualityInput: function () { + var quality = parseInt(this.$qualityInput.val() || 0); + // adapt the quality to what will actually happen + if (quality === 0) { + quality = 100; + } + // prevent flickering when clearing the input to type something else + if (!quality) { + return; + } + this.$qualityRange.val(quality); + this._onQualityRangeInput(); + }, + /** + * @private + */ + _onQualityRangeInput: function () { + var quality = parseInt(this.$qualityRange.val() || 0); + this.$qualityInput.val(quality); + this._updatePreview(); + this._validateForm(); + }, + /** + * @private + */ + _onOptimizeQualityInput: function () { + this._updatePreview(); + }, + /** + * @private + */ + _onWidthInput: function () { + var width = parseInt(this.$widthInput.val() || 0); + this.$heightInput + .val(parseInt(width / this.attachment.image_width * this.attachment.image_height)); + this._updatePreview(); + this._validateForm(); + }, + /** + * @private + */ + _onWidthPresetClick: function (ev) { + ev.preventDefault(); + this.$widthInput.val(parseInt($(ev.target).data('width'))); + this._onWidthInput(); + }, +}); + +return { + ImageOptimizeDialog: ImageOptimizeDialog, +}; +}); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index f398e23bff9..3303ec0a58f 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -1,16 +1,17 @@ odoo.define('wysiwyg.widgets.media', function (require) { 'use strict'; +var concurrency = require('web.concurrency'); +var config = require('web.config'); var core = require('web.core'); var Dialog = require('web.Dialog'); var dom = require('web.dom'); var fonts = require('wysiwyg.fonts'); +var ImageOptimizeDialog = require('wysiwyg.widgets.image_optimize_dialog').ImageOptimizeDialog; var utils = require('web.utils'); var Widget = require('web.Widget'); -var concurrency = require('web.concurrency'); var QWeb = core.qweb; - var _t = core._t; var MediaWidget = Widget.extend({ @@ -104,13 +105,14 @@ var SearchableMediaWidget = MediaWidget.extend({ var FileWidget = SearchableMediaWidget.extend({ events: _.extend({}, SearchableMediaWidget.prototype.events || {}, { 'click .o_upload_media_button': '_onUploadButtonClick', - 'click .o_upload_media_button_no_optimization': '_onUploadButtonNoOptimizationClick', + 'click .o_we_quick_upload': '_onQuickUploadClick', 'change .o_file_input': '_onFileInputChange', 'click .o_upload_media_url_button': '_onUploadURLButtonClick', 'input .o_we_url_input': '_onURLInputChange', 'click .o_existing_attachment_cell': '_onAttachmentClick', 'dblclick .o_existing_attachment_cell': '_onAttachmentDblClick', 'click .o_existing_attachment_remove': '_onRemoveClick', + 'click .o_existing_attachment_optimize': '_onExistingOptimizeClick', 'click .o_load_more': '_onLoadMoreClick', }), existingAttachmentsTemplate: undefined, @@ -118,6 +120,14 @@ var FileWidget = SearchableMediaWidget.extend({ IMAGE_MIMETYPES: ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'image/svg+xml'], NUMBER_OF_ATTACHMENTS_TO_DISPLAY: 30, + // This factor is used to take into account that an image displayed in a BS + // column might get bigger when displayed on a smaller breakpoint if that + // breakpoint leads to have less columns. + // Eg. col-lg-6 -> 480px per column -> col-md-12 -> 720px per column -> 1.5 + // However this will not be enough if going from 3 or more columns to 1, but + // in that case, we consider it a snippet issue. + OPTIMIZE_SIZE_FACTOR: 1.5, + /** * @constructor */ @@ -130,6 +140,7 @@ var FileWidget = SearchableMediaWidget.extend({ this.options = _.extend({ firstFilters: [], lastFilters: [], + showQuickUpload: config.debug, }, options || {}); this.attachments = []; @@ -138,6 +149,8 @@ var FileWidget = SearchableMediaWidget.extend({ this._onUploadURLButtonClick = dom.makeAsyncHandler(this._onUploadURLButtonClick); }, /** + * Loads all the existing images related to the target media. + * * @override */ willStart: function () { @@ -211,7 +224,7 @@ var FileWidget = SearchableMediaWidget.extend({ args: [], kwargs: { domain: this._getAttachmentsDomain(needle), - fields: ['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src'], + fields: ['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height'], order: [{name: 'id', asc: false}], context: this.options.context, }, @@ -257,6 +270,20 @@ var FileWidget = SearchableMediaWidget.extend({ .replace(allImgClasses, ' ') .replace(allImgClassModifiers, ' '); }, + /** + * Computes and returns the width that a new attachment should have to + * ideally occupy the space where it will be inserted. + * Only relevant for images. + * + * @see options.mediaWidth + * @see OPTIMIZE_SIZE_FACTOR + * + * @private + * @returns {integer} + */ + _computeOptimizedWidth: function () { + return Math.min(1920, parseInt(this.options.mediaWidth * this.OPTIMIZE_SIZE_FACTOR)); + }, /** * Returns the domain for attachments used in media dialog. * We look for attachments related to the current document. If there is a value for the model @@ -334,6 +361,43 @@ var FileWidget = SearchableMediaWidget.extend({ return this.search(this.$('.o_we_search').val() || ''); } }, + /** + * Opens the image optimize dialog for the given attachment. + * + * Hides the media dialog while the optimize dialog is open to avoid an + * overlap of modals. + * + * @private + * @param {object} attachment + * @param {boolean} isExisting: whether this is a new attachment that was + * just uploaded, or an existing attachment + * @returns {Promise} resolved with the updated attachment object when the + * optimize dialog is saved. Rejected if the dialog is otherwise closed. + */ + _openImageOptimizeDialog: function (attachment, isExisting) { + var self = this; + var promise = new Promise(function (resolve, reject) { + self.trigger_up('hide_parent_dialog_request'); + var optimizeDialog = new ImageOptimizeDialog(self, { + attachment: attachment, + isExisting: isExisting, + optimizedWidth: self._computeOptimizedWidth(), + }).open(); + optimizeDialog.on('attachment_updated', self, function (ev) { + optimizeDialog.off('closed'); + resolve(ev.data); + }); + optimizeDialog.on('closed', self, function () { + self.noSave = true; + resolve(attachment); + }); + }); + var always = function () { + self.trigger_up('show_parent_dialog_request'); + }; + promise.then(always).guardedCatch(always); + return promise; + }, /** * Renders the existing attachments and returns the result as a string. * @@ -446,7 +510,7 @@ var FileWidget = SearchableMediaWidget.extend({ */ _selectAttachement: function (attachment, save) { if (this.options.multiImages) { - // if the clicked image is already selected then unselect it + // if the clicked attachment is already selected then unselect it // unless it was a save request (then keep the current selection) var index = this.selectedAttachments.indexOf(attachment); if (index !== -1) { @@ -454,11 +518,11 @@ var FileWidget = SearchableMediaWidget.extend({ this.selectedAttachments.splice(index, 1); } } else { - // if the clicked image is not selected, add it to selection + // if the clicked attachment is not selected, add it to selected this.selectedAttachments.push(attachment); } } else { - // select the clicked image + // select the clicked attachment this.selectedAttachments = [attachment]; } this._highlightSelected(); @@ -478,7 +542,6 @@ var FileWidget = SearchableMediaWidget.extend({ this.$addUrlButton.toggleClass('btn-secondary', emptyValue) .toggleClass('btn-primary', !emptyValue) .prop('disabled', !isURL); - this.$urlSuccess.toggleClass('d-none', !isURL); this.$urlError.toggleClass('d-none', emptyValue || isURL); }, @@ -502,6 +565,23 @@ var FileWidget = SearchableMediaWidget.extend({ this._onAttachmentClick(ev, true); }, /** + * @private + */ + _onExistingOptimizeClick: function (ev) { + var self = this; + var $a = $(ev.currentTarget).closest('.o_existing_attachment_cell'); + var id = parseInt($a.data('id'), 10); + var attachment = _.findWhere(this.attachments, {id: id}); + ev.stopPropagation(); + return this._openImageOptimizeDialog(attachment, true).then(function (newAttachment) { + self._handleNewAttachment(newAttachment); + }); + }, + /** + * Handles change of the file input: create attachments with the new files + * and open the Preview dialog for each of them. Locks the save button until + * all new files have been processed. + * * @private * @returns {Promise} */ @@ -520,6 +600,7 @@ var FileWidget = SearchableMediaWidget.extend({ _addData: function () { var self = this; var uploadMutex = new concurrency.Mutex(); + var optimizeMutex = new concurrency.Mutex(); // Upload the smallest file first to block the user the least possible. var files = _.sortBy(this.$fileInput[0].files, 'size'); @@ -529,27 +610,55 @@ var FileWidget = SearchableMediaWidget.extend({ // limited by bandwidth. uploadMutex.exec(function () { return utils.getDataURLFromFile(file).then(function (result) { + var params = { + 'name': file.name, + 'data': result.split(',')[1], + 'res_id': self.options.res_id, + 'res_model': self.options.res_model, + 'filters': self.options.firstFilters.join('_'), + }; + if (self.quickUpload) { + params['width'] = self._computeOptimizedWidth(); + params['quality'] = 80; + } else { + params['width'] = 0; + params['quality'] = 0; + } return self._rpc({ route: '/web_editor/attachment/add_data', - params: { - 'name': file.name, - 'data': result.split(',')[1], - 'res_id': self.options.res_id, - 'res_model': self.options.res_model, - 'filters': self.options.firstFilters.join('_'), - }, + params: params, }).then(function (attachment) { - self._handleNewAttachment(attachment); + if (attachment.image_src && !self.quickUpload) { + optimizeMutex.exec(function () { + return self._openImageOptimizeDialog(attachment).then(function (updatedAttachment) { + self._handleNewAttachment(updatedAttachment); + }); + }); + } else { + self._handleNewAttachment(attachment); + } }); }); }); }); + return uploadMutex.getUnlockedDef().then(function () { - if (!self.options.multiImages) { - self.trigger_up('save_request'); - } + return optimizeMutex.getUnlockedDef().then(function () { + self.quickUpload = false; + if (!self.options.multiImages && !self.noSave) { + self.trigger_up('save_request'); + } + self.noSave = false; + }); }); }, + /** + * @private + */ + _onQuickUploadClick: function () { + this.quickUpload = true; + this.$uploadButton.trigger('click'); + }, /** * @private */ @@ -588,7 +697,7 @@ var FileWidget = SearchableMediaWidget.extend({ var emptyValue = (inputValue === ''); var isURL = /^.+\..+$/.test(inputValue); // TODO improve - var isImage = _.any(['.gif', '.jpe', '.jpg', '.png'], function (format) { + var isImage = _.any(['.gif', '.jpeg', '.jpe', '.jpg', '.png'], function (format) { return inputValue.endsWith(format); }); @@ -603,14 +712,6 @@ var FileWidget = SearchableMediaWidget.extend({ /** * @private */ - _onUploadButtonNoOptimizationClick: function () { - this.$('input[name="disable_optimization"]').val('1'); - this.$uploadButton.click(); - }, - /** - * @private - * @returns {Promise} - */ _onUploadURLButtonClick: function () { return this._mutex.exec(this._addUrl.bind(this)); }, diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js index 92c2f53099e..528cbd93bc6 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js @@ -28,6 +28,8 @@ var MediaDialog = Dialog.extend({ }), custom_events: _.extend({}, Dialog.prototype.custom_events || {}, { save_request: '_onSaveRequest', + show_parent_dialog_request: '_onShowRequest', + hide_parent_dialog_request: '_onHideRequest', }), /** @@ -209,6 +211,18 @@ var MediaDialog = Dialog.extend({ _onClickVideoTab: function () { this.activeWidget = this.videoWidget; }, + /** + * Handles hide request from child widgets. + * + * This is for usability, to allow hiding the modal for example when another + * smaller modal would be displayed on top. + * + * @private + * @param {OdooEvent} ev + */ + _onHideRequest: function (ev) { + this.$modal.addClass('d-none'); + }, /** * Handles save request from the child widgets. * @@ -222,6 +236,17 @@ var MediaDialog = Dialog.extend({ ev.stopPropagation(); this.save(); }, + /** + * Handles show request from the child widgets. + * + * This is for usability, it is the counterpart of @see _onHideRequest. + * + * @private + * @param {OdooEvent} ev + */ + _onShowRequest: function (ev) { + this.$modal.removeClass('d-none'); + }, }); return MediaDialog; diff --git a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js index 020a91b0a2b..f6eee549bfc 100644 --- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js +++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js @@ -853,6 +853,7 @@ registry.background = SnippetOption.extend({ var $editable = this.$target.closest('.o_editable'); var _editor = new weWidgets.MediaDialog(this, { onlyImages: true, + mediaWidth: 1920, firstFilters: ['background'], res_model: $editable.data('oe-model'), res_id: $editable.data('oe-id'), diff --git a/addons/web_editor/static/src/scss/wysiwyg.scss b/addons/web_editor/static/src/scss/wysiwyg.scss index 2cdb6ee1fca..af674a58a0b 100644 --- a/addons/web_editor/static/src/scss/wysiwyg.scss +++ b/addons/web_editor/static/src/scss/wysiwyg.scss @@ -301,25 +301,32 @@ body .modal { cursor: pointer; list-style: none; - // prevent hover/select border from flickering - border: 1px solid transparent; - - .o_existing_attachment_remove { - @include o-position-absolute(0, 0); + .o_existing_attachment_optimize, .o_existing_attachment_remove { background-color: scale-color($card-border-color, $alpha: -50%); text-shadow: 0 0 1px white; - border-radius: 0 0 0 2px; opacity: 0; cursor: pointer; &:hover { background-color: $card-border-color; + } + } + + .o_existing_attachment_optimize { + @include o-position-absolute($top: 0, $left: 0); + border-radius: 0 0 2px 0; + } + + .o_existing_attachment_remove { + @include o-position-absolute($top: 0, $right: 0); + border-radius: 0 0 0 2px; + &:hover { color: $o-wysiwyg-color-danger; } } &:hover { - .o_existing_attachment_remove { + .o_existing_attachment_optimize, .o_existing_attachment_remove { opacity: 1; } &.o_we_attachment_highlight, .o_we_attachment_highlight { @@ -455,6 +462,16 @@ body .modal { border-left: 1px solid gray('200'); } } + + .o_we_image_optimize_dialog { + .o_we_title_label { + font-size: 1rem; + } + .o_we_preview_area { + max-height: 400px; + overflow: auto; + } + } } // Highlight selected image/icon diff --git a/addons/web_editor/static/src/xml/wysiwyg.xml b/addons/web_editor/static/src/xml/wysiwyg.xml index a9e42a88231..abd2e940824 100644 --- a/addons/web_editor/static/src/xml/wysiwyg.xml +++ b/addons/web_editor/static/src/xml/wysiwyg.xml @@ -111,7 +111,6 @@
-
- @@ -186,6 +183,65 @@
+ +
+
+
+
+ + + Give a relevant name to your file to optimize search engine results. +
+ + Resizing is not supported for images of type . +
+
+
+ +
+
+ + + + + Reduce the size as much as possible to increase performance. +
+ + Changing the quality is not supported for images of type . +
+ +
+ + +
+ This reduces the quality to increase performance. +
+ + +
+
+ +
+
+ +
+
+ Reduce the quality as much as possible to increase performance. +
+
+
+
+

Preview with the current settings

+
+ Image Preview +
+
+
+
+
+
    @@ -198,6 +254,7 @@
  1. +
    diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js index 7d2a4b37bbb..0501baee4ea 100644 --- a/addons/web_editor/static/tests/test_utils.js +++ b/addons/web_editor/static/tests/test_utils.js @@ -359,6 +359,14 @@ function wysiwygData(data) { string: "image_src", type: 'char', }, + image_width: { + string: "image_width", + type: 'integer', + }, + image_height: { + string: "image_height", + type: 'integer', + }, }, records: [{ id: 1, @@ -373,6 +381,8 @@ function wysiwygData(data) { public: true, access_token: false, image_src: '/web_editor/static/src/img/transparent.png', + image_width: 256, + image_height: 256, }], generate_access_token: function () { return; diff --git a/addons/web_editor/views/editor.xml b/addons/web_editor/views/editor.xml index 7f68d0be5d8..5b1d7e8108a 100644 --- a/addons/web_editor/views/editor.xml +++ b/addons/web_editor/views/editor.xml @@ -41,6 +41,7 @@ +