summaryrefslogtreecommitdiff
path: root/addons/web_unsplash/static/src
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web_unsplash/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web_unsplash/static/src')
-rw-r--r--addons/web_unsplash/static/src/js/unsplash_beacon.js34
-rw-r--r--addons/web_unsplash/static/src/js/unsplash_image_widget.js259
-rw-r--r--addons/web_unsplash/static/src/js/unsplashapi.js89
-rw-r--r--addons/web_unsplash/static/src/scss/unsplash.scss11
-rw-r--r--addons/web_unsplash/static/src/xml/unsplash_image_widget.xml86
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>