Completed
Pull Request — master (#434)
by
unknown
02:15
created

SlideShow.show   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
nc 2
nop 0
1
/**
2
 * Nextcloud - Gallery
3
 *
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Olivier Paroz <[email protected]>
9
 *
10
 * @copyright Olivier Paroz 2017
11
 */
12
/* global Gallery, Thumbnails, DOMPurify */
13
(function ($, OC, OCA, t) {
14
	"use strict";
15
	/**
16
	 * Slideshow featuring zooming
17
	 *
18
	 * @constructor
19
	 */
20
	var SlideShow = function () {
21
	};
22
23
	SlideShow.prototype = {
24
		slideshowTemplate: null,
25
		container: null,
26
		zoomablePreviewContainer: null,
27
		controls: null,
28
		imageCache: {},
29
		/** {Image} */
30
		currentImage: null,
31
		errorLoadingImage: false,
32
		onStop: null,
33
		zoomablePreview: null,
34
		active: false,
35
		backgroundToggle: false,
36
		// We need 6 hexas for comparison reasons
37
		darkBackgroundColour: '#000000',
38
		lightBackgroundColour: '#ffffff',
39
40
		/**
41
		 * Initialises the slideshow
42
		 *
43
		 * @param {boolean} autoPlay
44
		 * @param {number} interval
45
		 * @param {Array} features
46
		 */
47
		init: function (autoPlay, interval, features) {
48
			if (features.indexOf('background_colour_toggle') > -1) {
49
				this.backgroundToggle = true;
50
			}
51
52
			return $.when(this._getSlideshowTemplate()).then(function ($tmpl) {
53
				// Move the slideshow outside the content so we can hide the content
54
				$('body').append($tmpl);
55
				this.container = $('#slideshow');
56
				this.zoomablePreviewContainer = this.container.find('.bigshotContainer');
57
				this.zoomablePreview = new SlideShow.ZoomablePreview(this.container);
58
				this.livePhotoPreview = new SlideShow.LivePreview(this.container);
59
				this.controls =
60
					new SlideShow.Controls(
61
						this,
62
						this.container,
63
						this.zoomablePreview,
64
						interval,
65
						features);
66
				this.controls.init();
67
68
				this._initControlsAutoFader();
69
70
				// Only modern browsers can manipulate history
71
				if (history && history.pushState) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable history is declared in the current environment, consider using typeof history === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
72
					// Stop the slideshow when backing out.
73
					$(window).bind('popstate.slideshow', function () {
74
						if (this.active === true) {
75
							this.active = false;
76
							this.controls.stop();
77
						}
78
					}.bind(this));
79
				}
80
			}.bind(this)).fail(function () {
81
				OC.Notification.show(t('gallery', 'Error loading slideshow template'));
82
			});
83
		},
84
85
		/**
86
		 * Refreshes the slideshow's data
87
		 *
88
		 * @param {{name:string, url: string, path: string, fallBack: string}[]} images
89
		 * @param {boolean} autoPlay
90
		 */
91
		setImages: function (images, autoPlay) {
92
			this._hideImage();
93
			this.images = images;
94
			this.controls.update(images, autoPlay);
95
		},
96
97
		/**
98
		 * Launches the slideshow
99
		 *
100
		 * @param {number} index
101
		 *
102
		 * @returns {*}
103
		 */
104
		show: function (index) {
105
			this.hideErrorNotification();
106
			this.active = true;
107
			// Clean any live photo container
108
			this.livePhotoPreview.reset();
109
			this.container.show();
110
			this.container.css('background-position', 'center');
111
			this._hideImage();
112
			this.container.find('.icon-loading-dark').show();
113
			var currentImageId = index;
114
			return this.loadImage(this.images[index]).then(function (img) {
115
				this.container.css('background-position', '-10000px 0');
116
117
				// check if we moved along while we were loading
118
				if (currentImageId === index) {
119
					this.zoomablePreviewContainer.css('display', 'none');
120
					var image = this.images[index];
121
					var transparent = this._isTransparent(image.mimeType);
122
					this.controls.showActionButtons(transparent, Gallery.token, image.permissions);
123
					this.errorLoadingImage = false;
124
					this.currentImage = img;
125
					img.setAttribute('alt', image.name);
126
					$.when(this.livePhotoPreview.startLivePreview(image, this.currentImage)).fail(function() {
127
						this.zoomablePreviewContainer.css('display', 'block');
128
						$(img).css('position', 'absolute');
129
						$(img).css('background-color', image.backgroundColour);
130
						if (transparent && this.backgroundToggle === true) {
131
							var $border = 30 / window.devicePixelRatio;
132
							$(img).css('outline', $border + 'px solid ' + image.backgroundColour);
133
						}
134
135
						this.zoomablePreview.startBigshot(img, this.currentImage, image.mimeType);
136
					}.bind(this));
137
					this._setUrl(image.path);
138
					this.controls.show(currentImageId);
139
					this.container.find('.icon-loading-dark').hide();
140
				}
141
			}.bind(this), function () {
142
				// Don't do anything if the user has moved along while we were loading as it would
143
				// mess up the index
144
				if (currentImageId === index) {
145
					this.errorLoadingImage = true;
146
					this.showErrorNotification(null);
147
					this._setUrl(this.images[index].path);
148
					this.images.splice(index, 1);
149
					this.controls.updateControls(this.images, this.errorLoadingImage);
150
				}
151
			}.bind(this));
152
		},
153
154
		/**
155
		 * Loads the image to show in the slideshow and preloads the next one
156
		 *
157
		 * @param {Object} preview
158
		 *
159
		 * @returns {*}
160
		 */
161
		loadImage: function (preview) {
162
			var url = preview.url;
163
			var mimeType = preview.mimeType;
164
165
			if (!this.imageCache[url]) {
166
				this.imageCache[url] = new $.Deferred();
167
				var image = new Image();
0 ignored issues
show
Bug introduced by
The variable Image seems to be never declared. If this is a global, consider adding a /** global: Image */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
168
169
				image.onload = function () {
170
					preview.backgroundColour = this._getBackgroundColour(image, mimeType);
171
					if (this.imageCache[url]) {
172
						this.imageCache[url].resolve(image);
173
					}
174
				}.bind(this);
175
				image.onerror = function () {
176
					if (this.imageCache[url]) {
177
						this.imageCache[url].reject(url);
178
					}
179
				}.bind(this);
180
				if (mimeType === 'image/svg+xml') {
181
					image.src = this._getSVG(url);
182
				} else {
183
					image.src = url;
184
				}
185
			}
186
			return this.imageCache[url];
187
		},
188
189
		/**
190
		 * Shows a new image in the slideshow and preloads the next in the list
191
		 *
192
		 * @param {number} current
193
		 * @param {Object} next
194
		 */
195
		next: function (current, next) {
196
			this.show(current).then(function () {
197
				// Preloads the next image in the list
198
				this.loadImage(next);
199
			}.bind(this));
200
		},
201
202
		/**
203
		 * Determines which colour to use for the background
204
		 *
205
		 * @param {*} image
206
		 * @param {string} mimeType
207
		 *
208
		 * @returns {string}
209
		 * @private
210
		 */
211
		_getBackgroundColour: function (image, mimeType) {
212
			var backgroundColour = this.darkBackgroundColour;
213
			if (this._isTransparent(mimeType) && this._isMainlyDark(image)) {
214
				backgroundColour = this.lightBackgroundColour;
215
			}
216
			return backgroundColour;
217
		},
218
219
		/**
220
		 * Calculates the luminance of an image to determine if an image is mainly dark
221
		 *
222
		 * @param {*} image
223
		 *
224
		 * @returns {boolean}
225
		 * @private
226
		 */
227
		_isMainlyDark: function (image) {
228
			var isMainlyDark = false;
229
			var numberOfSamples = 1000; // Seems to be the sweet spot
230
			// The name has to be 'canvas'
231
			var lumiCanvas = document.createElement('canvas');
232
233
			var imgArea = image.width * image.height;
234
			var canArea = numberOfSamples;
235
			var factor = Math.sqrt(canArea / imgArea);
236
237
			var scaledWidth = factor * image.width;
238
			var scaledHeight = factor * image.height;
239
			lumiCanvas.width = scaledWidth;
240
			lumiCanvas.height = scaledHeight;
241
			var lumiCtx = lumiCanvas.getContext('2d');
242
			lumiCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
243
			var imgData = lumiCtx.getImageData(0, 0, lumiCanvas.width, lumiCanvas.height);
244
			var pix = imgData.data; // pix.length will be approximately 4*numberOfSamples (for RGBA)
245
			var pixelArraySize = pix.length;
246
			var totalLuminance = 0;
247
			var sampleNumber = 1;
248
			var averageLuminance;
249
			var totalAlpha = 0;
250
			var alphaLevel;
251
			var red = 0;
252
			var green = 0;
253
			var blue = 0;
254
			var alpha = 0;
255
			var lum = 0;
256
			var alphaThreshold = 0.1;
257
258
			var sampleCounter = 0;
259
			var itemsPerPixel = 4; // red, green, blue, alpha
260
			// i += 4 because 4 colours for every pixel
261
			for (var i = 0, n = pixelArraySize; i < n; i += itemsPerPixel) {
262
				sampleCounter++;
263
				alpha = pix[i + 3] / 255;
264
				totalAlpha += alpha;
265
				if (Math.ceil(alpha * 100) / 100 > alphaThreshold) {
266
					red = pix[i];
267
					green = pix[i + 1];
268
					blue = pix[i + 2];
269
					// Luminance formula from
270
					// http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
271
					lum = (red + red + green + green + green + blue) / 6;
272
					//lum = (red * 0.299 + green * 0.587 + blue * 0.114 );
273
					totalLuminance += lum * alpha;
274
					sampleNumber++;
275
				}
276
			}
277
278
			// Deletes the canvas
279
			lumiCanvas = null;
0 ignored issues
show
Unused Code introduced by
The assignment to lumiCanvas seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
280
281
			// Calculate the optimum background colour for this image
282
			averageLuminance = Math.ceil((totalLuminance / sampleNumber) * 100) / 100;
283
			alphaLevel = Math.ceil((totalAlpha / numberOfSamples) * 100);
284
285
			if (averageLuminance < 60 && alphaLevel < 90) {
286
				isMainlyDark = true;
287
			}
288
289
			return isMainlyDark;
290
		},
291
292
		/**
293
		 * Stops the slideshow
294
		 */
295
		stop: function () {
296
			this.active = false;
297
			this.images = null;
298
			this._hideImage();
299
			if (this.onStop) {
300
				this.onStop();
301
			}
302
		},
303
304
		/**
305
		 * Sends the current image as a download
306
		 *
307
		 * @param {string} downloadUrl
308
		 *
309
		 * @returns {boolean}
310
		 */
311
		getImageDownload: function (downloadUrl) {
312
			OC.redirect(downloadUrl);
313
			return false;
314
		},
315
316
		/**
317
		 * Changes the colour of the background of the image
318
		 */
319
		toggleBackground: function () {
320
			var toHex = function (x) {
321
				return ("0" + parseInt(x).toString(16)).slice(-2);
322
			};
323
			var container = this.zoomablePreviewContainer.children('img');
324
			var rgb = container.css('background-color').match(/\d+/g);
325
			var hex = "#" + toHex(rgb[0]) + toHex(rgb[1]) + toHex(rgb[2]);
326
			var $border = 30 / window.devicePixelRatio;
327
			var newBackgroundColor;
328
329
			// Grey #363636
330
			if (hex === this.darkBackgroundColour) {
331
				newBackgroundColor = this.lightBackgroundColour;
332
			} else {
333
				newBackgroundColor = this.darkBackgroundColour;
334
			}
335
336
			container.css('background-color', newBackgroundColor);
337
			if (this.backgroundToggle === true) {
338
				container.css('outline', $border + 'px solid ' + newBackgroundColor);
339
			}
340
		},
341
342
		/**
343
		 * Shows an error notification
344
		 *
345
		 * @param {string} message
346
		 */
347
		showErrorNotification: function (message) {
348
			if ($.isEmptyObject(message)) {
349
				message = t('gallery',
350
					'<strong>Error!</strong> Could not generate a preview of this file.<br>' +
351
					'Please go to the next slide while we remove this image from the slideshow');
352
			}
353
			this.container.find('.notification').html(message);
354
			this.container.find('.notification').show();
355
			this.controls.hideButton('.changeBackground');
356
		},
357
358
		/**
359
		 * Hides the error notification
360
		 */
361
		hideErrorNotification: function () {
362
			this.container.find('.notification').hide();
363
			this.container.find('.notification').html('');
364
		},
365
366
		/**
367
		 * Removes a specific button from the interface
368
		 *
369
		 * @param button
370
		 */
371
		removeButton: function (button) {
372
			this.controls.removeButton(button);
373
		},
374
375
		/**
376
		 * Deletes an image from the slideshow
377
		 *
378
		 * @param {object} image
379
		 * @param {number} currentIndex
380
		 */
381
		deleteImage: function (image, currentIndex) {
382
			// These are Gallery specific commands to be replaced
383
			// which should sit somewhere else
384
			if (!window.galleryFileAction) {
385
				delete Gallery.imageMap[image.path];
386
				delete Thumbnails.map[image.file];
387
				Gallery.albumMap[Gallery.currentAlbum].images.splice(currentIndex, 1);
388
				Gallery.view.init(Gallery.currentAlbum);
389
			}
390
		},
391
392
		/**
393
		 * Automatically fades the controls after 3 seconds
394
		 *
395
		 * @private
396
		 */
397
		_initControlsAutoFader: function () {
398
			var inactiveCallback = function () {
399
				this.container.addClass('inactive');
400
			}.bind(this);
401
			var inactiveTimeout = setTimeout(inactiveCallback, 3000);
402
403
			this.container.on('mousemove touchstart', function () {
404
				this.container.removeClass('inactive');
405
				clearTimeout(inactiveTimeout);
406
				inactiveTimeout = setTimeout(inactiveCallback, 3000);
407
			}.bind(this));
408
		},
409
410
		/**
411
		 * Simplest way to detect if image is transparent.
412
		 *
413
		 * That's very inaccurate since it doesn't include images which support transparency
414
		 *
415
		 * @param mimeType
416
		 * @returns {boolean}
417
		 * @private
418
		 */
419
		_isTransparent: function (mimeType) {
420
			return !(mimeType === 'image/jpeg'
421
				|| mimeType === 'image/x-dcraw'
422
				|| mimeType === 'application/font-sfnt'
423
				|| mimeType === 'application/x-font'
424
			);
425
		},
426
427
		/**
428
		 * Changes the browser Url, based on the current image
429
		 *
430
		 * @param {string} path
431
		 * @private
432
		 */
433
		_setUrl: function (path) {
434
			if (history && history.replaceState) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable history is declared in the current environment, consider using typeof history === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
435
				history.replaceState('', '', '#' + encodeURI(path));
436
			}
437
		},
438
439
		/**
440
		 * Hides the current image (before loading the next)
441
		 *
442
		 * @private
443
		 */
444
		_hideImage: function () {
445
			this.zoomablePreviewContainer.empty();
446
			this.controls.hideActionButtons();
447
		},
448
449
		/**
450
		 * Retrieves an SVG
451
		 *
452
		 * An SVG can't be simply attached to a src attribute like a bitmap image
453
		 *
454
		 * @param {string} source
455
		 *
456
		 * @returns {*}
457
		 * @private
458
		 */
459
		_getSVG: function (source) {
460
			var svgPreview = null;
461
			// DOMPurify only works with IE10+ and we load SVGs in the IMG tag
462
			if (window.btoa &&
463
				document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image",
464
					"1.1")) {
465
				var xmlHttp = new XMLHttpRequest();
0 ignored issues
show
Bug introduced by
The variable XMLHttpRequest seems to be never declared. If this is a global, consider adding a /** global: XMLHttpRequest */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
466
				xmlHttp.open("GET", source, false);
467
				xmlHttp.send(null);
468
				if (xmlHttp.status === 200) {
469
					var pureSvg = DOMPurify.sanitize(xmlHttp.responseText, {ADD_TAGS: ['filter']});
470
					// Remove XML comment garbage left in the purified data
471
					var badTag = pureSvg.indexOf(']&gt;');
472
					var fixedPureSvg = pureSvg.substring(badTag < 0 ? 0 : 5, pureSvg.length);
473
					svgPreview = "data:image/svg+xml;base64," + window.btoa(fixedPureSvg);
474
				}
475
			}
476
477
			return svgPreview;
478
		},
479
480
		/**
481
		 * Retrieves the slideshow's template
482
		 *
483
		 * @returns {*}
484
		 * @private
485
		 */
486
		_getSlideshowTemplate: function () {
487
			var defer = $.Deferred();
488
			if (!this.$slideshowTemplate) {
489
				var self = this;
490
				var url = OC.generateUrl('apps/gallery/slideshow', null);
491
				$.get(url, function (tmpl) {
492
						var template = $(tmpl);
493
						var tmplButton;
494
						var buttonsArray = [
495
							{
496
								el: '.next',
497
								trans: t('gallery', 'Next')
498
							},
499
							{
500
								el: '.play',
501
								trans: t('gallery', 'Play'),
502
								toolTip: true
503
							},
504
							{
505
								el: '.pause',
506
								trans: t('gallery', 'Pause'),
507
								toolTip: true
508
							},
509
							{
510
								el: '.previous',
511
								trans: t('gallery', 'Previous')
512
							},
513
							{
514
								el: '.exit',
515
								trans: t('gallery', 'Close'),
516
								toolTip: true
517
							},
518
							{
519
								el: '.downloadImage',
520
								trans: t('gallery', 'Download'),
521
								toolTip: true
522
							},
523
							{
524
								el: '.changeBackground',
525
								trans: t('gallery', 'Toggle background'),
526
								toolTip: true
527
							},
528
							{
529
								el: '.deleteImage',
530
								trans: t('gallery', 'Delete'),
531
								toolTip: true
532
							},
533
							{
534
								el: '.shareImage',
535
								trans: t('gallery', 'Share'),
536
								toolTip: true
537
							}
538
						];
539
						for (var i = 0; i < buttonsArray.length; i++) {
540
							var button = buttonsArray[i];
541
542
							tmplButton = template.find(button.el);
543
							tmplButton.val(button.trans);
544
							if (button.toolTip) {
545
								tmplButton.attr("title", button.trans);
546
							}
547
						}
548
						self.$slideshowTemplate = template;
549
						defer.resolve(self.$slideshowTemplate);
550
					})
551
					.fail(function () {
552
						defer.reject();
553
					});
554
			} else {
555
				defer.resolve(this.$slideshowTemplate);
556
			}
557
			return defer.promise();
558
		}
559
	};
560
561
	window.SlideShow = SlideShow;
562
})(jQuery, OC, OCA, t);
563