diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web_unsplash/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web_unsplash/static/src')
5 files changed, 479 insertions, 0 deletions
diff --git a/addons/web_unsplash/static/src/js/unsplash_beacon.js b/addons/web_unsplash/static/src/js/unsplash_beacon.js new file mode 100644 index 00000000..c15ff027 --- /dev/null +++ b/addons/web_unsplash/static/src/js/unsplash_beacon.js @@ -0,0 +1,34 @@ +odoo.define('web_unsplash.beacon', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); + +publicWidget.registry.UnsplashBeacon = publicWidget.Widget.extend({ + // /!\ To adapt the day the beacon makes sense for backend customizations + selector: '#wrapwrap', + + /** + * @override + */ + start: function () { + var unsplashImages = _.map(this.$('img[src*="/unsplash/"]'), function (img) { + // get image id from URL (`http://www.domain.com:1234/unsplash/xYdf5feoI/lion.jpg` -> `xYdf5feoI`) + return img.src.split('/unsplash/')[1].split('/')[0]; + }); + if (unsplashImages.length) { + this._rpc({ + route: '/web_unsplash/get_app_id', + }).then(function (appID) { + if (!appID) { + return; + } + $.get('https://views.unsplash.com/v', { + 'photo_id': unsplashImages.join(','), + 'app_id': appID, + }); + }); + } + return this._super.apply(this, arguments); + }, +}); +}); diff --git a/addons/web_unsplash/static/src/js/unsplash_image_widget.js b/addons/web_unsplash/static/src/js/unsplash_image_widget.js new file mode 100644 index 00000000..e638806d --- /dev/null +++ b/addons/web_unsplash/static/src/js/unsplash_image_widget.js @@ -0,0 +1,259 @@ +odoo.define('web_unsplash.image_widgets', function (require) { +'use strict'; + +var core = require('web.core'); +var UnsplashAPI = require('unsplash.api'); +var widgetsMedia = require('wysiwyg.widgets.media'); + +var unsplashAPI = null; + +// Prevent base class from treating unsplash images like regular attachments +const originalEvents = widgetsMedia.ImageWidget.prototype.events; +const clickHandler = originalEvents['click .o_existing_attachment_cell']; +if (!clickHandler) { + throw new Error(`Couldn't find a handler for o_existing_attachment_cell clicks. +The unsplash image widget needs to prevent this handler from executing on unsplash attachments.`); +} +_.extend(originalEvents, { + 'click .o_existing_attachment_cell:not(.o_unsplash_attachment_cell)': clickHandler, +}); +delete originalEvents['click .o_existing_attachment_cell']; + +widgetsMedia.ImageWidget.include({ + xmlDependencies: widgetsMedia.ImageWidget.prototype.xmlDependencies.concat( + ['/web_unsplash/static/src/xml/unsplash_image_widget.xml'] + ), + events: _.extend({}, widgetsMedia.ImageWidget.prototype.events, { + 'click .o_unsplash_attachment_cell[data-imgid]': '_onUnsplashImgClick', + 'click button.save_unsplash': '_onSaveUnsplashCredentials', + }), + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + + this._unsplash = { + selectedImages: {}, + isMaxed: false, + query: false, + error: false, + records: [], + }; + + // TODO improve this + // + // This is a `hack` to prevent the UnsplashAPI to be destroyed every + // time the media dialog is closed. Indeed, UnsplashAPI has a cache + // system to recude unsplash call, it is then better to keep its state + // to take advantage from it from one media dialog call to another. + // + // Unsplash API will either be (it's still being discussed): + // * a service (ideally coming with an improvement to not auto load + // the service) + // * initialized in the website_root (trigger_up) + if (unsplashAPI === null) { + this.unsplashAPI = new UnsplashAPI(this); + unsplashAPI = this.unsplashAPI; + } else { + this.unsplashAPI = unsplashAPI; + this.unsplashAPI.setParent(this); + } + }, + /** + * @override + */ + destroy: function () { + // TODO See `hack` explained in `init`. This prevent the media dialog destroy + // to destroy unsplashAPI when destroying the children + this.unsplashAPI.setParent(undefined); + this._super.apply(this, arguments); + }, + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + /** + * @override + */ + _save: async function () { + const _super = this._super; + if (Object.keys(this._unsplash.selectedImages).length) { + this.saved = true; + const images = await this._rpc({ + route: '/web_unsplash/attachment/add', + params: { + unsplashurls: this._unsplash.selectedImages, + res_model: this.options.res_model, + res_id: this.options.res_id, + query: this._unsplash.query, + }, + }); + this.attachments.push(...images); + this.selectedAttachments.push(...images); + } + return _super.apply(this, arguments); + }, + /** + * @override + */ + search: async function (needle) { + var self = this; + await this._super(...arguments); + + this._unsplash.query = needle; + if (!needle) { + this._unsplash.records = []; + return; + } + + await this.unsplashAPI.getImages(needle, this.numberOfAttachmentsToDisplay).then(function (res) { + self._unsplash.isMaxed = res.isMaxed; + self._unsplash.records = res.images; + self._unsplash.error = false; + }, function (err) { + self._unsplash.error = err; + }); + }, + /** + * @override + */ + hasContent() { + if (this.searchService === 'all') { + return this._super(...arguments) || (this.unsplashRecords && this.unsplashRecords.length); + } else if (this.searchService === 'unsplash') { + return (this.unsplashRecords && this.unsplashRecords.length); + } + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _highlightSelected: function () { + this._super.apply(this, arguments); + + const $select = this.$('.o_unsplash_attachment_cell[data-imgid]').filter((i, el) => { + return $(el).data('imgid') in this._unsplash.selectedImages; + }).addClass('o_we_attachment_selected'); + return $select; + }, + /** + * @private + */ + _loadMoreImages: function (forceSearch) { + if (!this.$('.o_we_search').val()) { + return this._super(forceSearch); + } + this.numberOfAttachmentsToDisplay += 10; + this.search(this.$('.o_we_search').val()).then(() => this._renderThumbnails()); + }, + /** + * @override + */ + _renderThumbnails: function () { + this._super(...arguments); + this.$('.unsplash_error').empty(); + if (!['all', 'unsplash'].includes(this.searchService)) { + return; + } + if (this._unsplash.query && this._unsplash.error) { + this.$('.unsplash_error').html( + core.qweb.render('web_unsplash.dialog.error.content', { + status: this._unsplash.error, + }) + ); + return; + } + + if (['all', 'unsplash'].includes(this.searchService) && this._unsplash.query && !this._unsplash.isMaxed) { + this.$('.o_load_more').removeClass('d-none'); + this.$('.o_load_done_msg').addClass('d-none'); + } + }, + /** + * @override + */ + _renderExisting: function (attachments) { + this.unsplashRecords = this._unsplash.records.map(record => { + const url = new URL(record.urls.regular); + // In small windows, row height could get quite a bit larger than the min, so we keep some leeway. + url.searchParams.set('h', 2 * this.MIN_ROW_HEIGHT); + url.searchParams.delete('w'); + return Object.assign({}, record, { + url: url.toString(), + }); + }); + return this._super(...arguments); + }, + /** + * @override + */ + _selectAttachement: function (attachment, save) { + if (!this.options.multiImages) { + this._unsplash.selectedImages = {}; + } + this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onSaveUnsplashCredentials: function () { + var self = this; + var key = this.$('#accessKeyInput').val().trim(); + var appId = this.$('#appIdInput').val().trim(); + + this.$('#accessKeyInput').toggleClass('is-invalid', !key); + this.$('#appIdInput').toggleClass('is-invalid', !appId); + + if (key && appId) { + if (!this.$el.find('.is-invalid').length) { + this._rpc({ + route: '/web_unsplash/save_unsplash', + params: {key: key, appId: appId}, + }).then(function () { + self.unsplashAPI.clientId = key; + self._unsplash.error = false; + self.search(self._unsplash.query).then(() => self._renderThumbnails()); + }); + } + } + }, + /** + * @private + */ + _onUnsplashImgClick: function (ev) { + if (this.saved) { + // already saved, probably a double click. Ignore. + return; + } + const {imgid, url, downloadUrl, description} = ev.currentTarget.dataset; + if (!this.options.multiImages) { + this._unsplash.selectedImages = {}; + this.selectedAttachments = []; + } + if (imgid in this._unsplash.selectedImages) { + delete this._unsplash.selectedImages[imgid]; + } else { + const _1920Url = new URL(url); + _1920Url.searchParams.set('w', '1920'); + this._unsplash.selectedImages[imgid] = {url: _1920Url.href, download_url: downloadUrl, description: description}; + } + this._highlightSelected(); + if (!this.options.multiImages) { + this.trigger_up('save_request'); + } + }, +}); +}); diff --git a/addons/web_unsplash/static/src/js/unsplashapi.js b/addons/web_unsplash/static/src/js/unsplashapi.js new file mode 100644 index 00000000..8156be6c --- /dev/null +++ b/addons/web_unsplash/static/src/js/unsplashapi.js @@ -0,0 +1,89 @@ +odoo.define('unsplash.api', function (require) { +'use strict'; + +var Class = require('web.Class'); +var rpc = require('web.rpc'); +var Mixins = require('web.mixins'); +var ServicesMixin = require('web.ServicesMixin'); + +var UnsplashCore = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { + /** + * @constructor + */ + init: function (parent) { + Mixins.EventDispatcherMixin.init.call(this, arguments); + this.setParent(parent); + + this._cache = {}; + this.clientId = false; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Gets unsplash images from query string. + * + * @param {String} query search terms + * @param {Integer} pageSize number of image to display per page + * @returns {Promise} + */ + getImages: function (query, pageSize) { + var from = 0; + var to = pageSize; + var cachedData = this._cache[query]; + + if (cachedData && (cachedData.images.length >= to || (cachedData.totalImages !== 0 && cachedData.totalImages < to))) { + return Promise.resolve({ images: cachedData.images.slice(from, to), isMaxed: to > cachedData.totalImages }); + } + return this._fetchImages(query).then(function (cachedData) { + return { images: cachedData.images.slice(from, to), isMaxed: to > cachedData.totalImages }; + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Fetches images from unsplash and stores it in cache + * + * @param {String} query search terms + * @returns {Promise} + * @private + */ + _fetchImages: function (query) { + if (!this._cache[query]) { + this._cache[query] = { + images: [], + maxPages: 0, + totalImages: 0, + pageCached: 0 + }; + } + var cachedData = this._cache[query]; + var payload = { + query: query, + page: cachedData.pageCached + 1, + per_page: 30, // max size from unsplash API + }; + return this._rpc({ + route: '/web_unsplash/fetch_images', + params: payload, + }).then(function (result) { + if (result.error) { + return Promise.reject(result.error); + } + cachedData.pageCached++; + cachedData.images.push.apply(cachedData.images, result.results); + cachedData.maxPages = result.total_pages; + cachedData.totalImages = result.total; + return cachedData; + }); + }, +}); + +return UnsplashCore; + +}); diff --git a/addons/web_unsplash/static/src/scss/unsplash.scss b/addons/web_unsplash/static/src/scss/unsplash.scss new file mode 100644 index 00000000..9e0267c2 --- /dev/null +++ b/addons/web_unsplash/static/src/scss/unsplash.scss @@ -0,0 +1,11 @@ +.unsplash_error { + padding: 30px 0; + .access_key_box { + padding: 9px; + background-color: #fcfcfc; + border: 1px solid #ededee; + input { + min-width: 300px; + } + } +} diff --git a/addons/web_unsplash/static/src/xml/unsplash_image_widget.xml b/addons/web_unsplash/static/src/xml/unsplash_image_widget.xml new file mode 100644 index 00000000..0bd4a70f --- /dev/null +++ b/addons/web_unsplash/static/src/xml/unsplash_image_widget.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + +<t t-extend="wysiwyg.widgets.file"> + <t t-jquery=".o_we_load_more" t-operation="after"> + <div class="unsplash_error"></div> + </t> +</t> +<t t-extend="wysiwyg.widgets.image"> + <t t-jquery="option[value='media-library']" t-operation="after"> + <option value="unsplash">Photos (via Unsplash)</option> + </t> +</t> +<t t-extend="wysiwyg.widgets.image.existing.attachments"> + <t t-jquery="t[t-foreach='libraryMedia']" t-operation="after"> + <t t-if="['all', 'unsplash'].includes(widget.searchService)" t-foreach="widget.unsplashRecords" t-as="record"> + <t t-call="web_unsplash.dialog.image.content"/> + </t> + </t> +</t> + +<t t-name="web_unsplash.dialog.image.content"> + <div class="o_existing_attachment_cell o_unsplash_attachment_cell position-relative align-items-center justify-content-center bg-light" t-att-data-imgid="record.id" t-att-data-id="record.id" t-att-data-url="record.urls.regular" t-att-data-download-url="record.links.download_location" t-att-data-description="record.alt_description"> + <img class="img img-fluid o_we_attachment_highlight" t-att-src="record.url" t-att-alt="record.alt_description" style="max-height: 100%;"/> + <a class="o_we_media_author" t-att-href="record.user.links.html" target="_blank" t-esc="record.user.name" t-att-title="record.user.name"/> + </div> +</t> + +<t t-name="web_unsplash.dialog.error.credentials"> + <h4><t t-esc="title"/></h4> + <div class="details"> + <t t-esc="subtitle"/> + <div class="form-group mt-4 access_key_box"> + <input type="text" class="form-control w-100" id="accessKeyInput" placeholder="Paste your access key here"/> + </div> + <a href="https://www.odoo.com/documentation/14.0/applications/general/unsplash/unsplash_access_key.html" target="_blank"><i class="fa fa-arrow-right"/> Generate an access key</a> + <div class="form-group mt-4 access_key_box"> + <input type="text" class="form-control w-100" id="appIdInput" placeholder="Paste your application ID here"/> + </div> + <a href="https://www.odoo.com/documentation/14.0/applications/general/unsplash/unsplash_application_id.html" target="_blank"> + <i class="fa fa-arrow-right"/> How to find my Unsplash Application ID?</a> + <button type="button" class="btn btn-primary btn-block mt-4 save_unsplash">Apply</button> + </div> +</t> + +<t t-name="web_unsplash.dialog.error.content"> + <div class="d-flex mt-2 unsplash_error"> + <div class="mx-auto text-center"> + <t t-if="status == 'key_not_found'"> + <t t-call="web_unsplash.dialog.error.credentials"> + <t t-set="title"> + Unsplash requires an access key and an application ID + </t> + </t> + </t> + <t t-elif="status == 403"> + <h4 class="text-muted"> + Search is temporarily unavailable + </h4> + <div class="details"> + The max number of searches is exceeded. Please retry in an hour or extend to a better account. + </div> + </t> + <t t-elif="status == 401"> + <t t-call="web_unsplash.dialog.error.credentials"> + <t t-set="title"> + Unauthorized Key + </t> + <t t-set="subtitle"> + Please check your Unsplash access key and application ID. + </t> + </t> + </t> + <t t-else=""> + <h4 class="text-muted"> + Something went wrong + </h4> + <div class="details"> + Please check your internet connection or contact administrator. + </div> + </t> + </div> + </div> +</t> + +</templates> |
