Issues (263)

js/slideshow.js (1 issue)

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.controls =
59
					new SlideShow.Controls(
60
						this,
61
						this.container,
62
						this.zoomablePreview,
63
						interval,
64
						features,
65
						this.restoreContent.bind(this));
66
				this.controls.init();
67
68
				this._initControlsAutoFader();
69
70
				// Only modern browsers can manipulate history
71
				if (history && history.pushState) {
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
		 * Hides the content behind the slideshow
99
		 *
100
		 * This should be called when the slideshow is shown.
101
		 *
102
		 * It hides the content (and, in the public share page, also the footer)
103
		 * to ensure that the body size is just the slideshow size and thus no
104
		 * scroll bars are shown.
105
		 */
106
		hideContent: function () {
107
			this._savedScrollPosition = $(window).scrollTop();
108
109
			$('#content').hide();
110
			$('footer').hide();
111
		},
112
113
		/**
114
		 * Shows again the content behind the slideshow
115
		 *
116
		 * This should be called when the slideshow is hidden.
117
		 *
118
		 * It restores the content hidden when calling "hideContent", including
119
		 * the vertical scrolling position.
120
		 */
121
		restoreContent: function () {
122
			$('#content').show();
123
			$('footer').show();
124
125
			if (this._savedScrollPosition) {
126
				$(window).scrollTop(this._savedScrollPosition);
127
			}
128
		},
129
130
		/**
131
		 * Launches the slideshow
132
		 *
133
		 * @param {number} index
134
		 *
135
		 * @returns {*}
136
		 */
137
		show: function (index) {
138
			this.hideErrorNotification();
139
			this.active = true;
140
			this.container.show();
141
			this.hideContent();
142
			this.container.css('background-position', 'center');
143
			this._hideImage();
144
			this.container.find('.icon-loading-dark').show();
145
			var currentImageId = index;
146
			return this.loadImage(this.images[index]).then(function (img) {
147
				this.container.css('background-position', '-10000px 0');
148
149
				// check if we moved along while we were loading
150
				if (currentImageId === index) {
151
					var image = this.images[index];
152
					var transparent = this._isTransparent(image.mimeType);
153
					this.controls.showActionButtons(transparent, Gallery.token, image.permissions);
154
					this.errorLoadingImage = false;
155
					this.currentImage = img;
156
					img.setAttribute('alt', image.name);
157
					$(img).css('position', 'absolute');
158
					$(img).css('background-color', image.backgroundColour);
159
					if (transparent && this.backgroundToggle === true) {
160
						var $border = 30 / window.devicePixelRatio;
161
						$(img).css('outline', $border + 'px solid ' + image.backgroundColour);
162
					}
163
164
					this.zoomablePreview.startBigshot(img, this.currentImage, image.mimeType);
165
166
					this._setUrl(image.path);
167
					this.controls.show(currentImageId);
168
					this.container.find('.icon-loading-dark').hide();
169
				}
170
			}.bind(this), function () {
171
				// Don't do anything if the user has moved along while we were loading as it would
172
				// mess up the index
173
				if (currentImageId === index) {
174
					this.errorLoadingImage = true;
175
					this.showErrorNotification(null);
176
					this._setUrl(this.images[index].path);
177
					this.images.splice(index, 1);
178
					this.controls.updateControls(this.images, this.errorLoadingImage);
179
				}
180
			}.bind(this));
181
		},
182
183
		/**
184
		 * Loads the image to show in the slideshow and preloads the next one
185
		 *
186
		 * @param {Object} preview
187
		 *
188
		 * @returns {*}
189
		 */
190
		loadImage: function (preview) {
191
			var url = preview.url;
192
			var mimeType = preview.mimeType;
193
194
			if (!this.imageCache[url]) {
195
				this.imageCache[url] = new $.Deferred();
196
				var image = new Image();
0 ignored issues
show
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...
197
198
				image.onload = function () {
199
					preview.backgroundColour = this._getBackgroundColour(image, mimeType);
200
					if (this.imageCache[url]) {
201
						this.imageCache[url].resolve(image);
202
					}
203
				}.bind(this);
204
				image.onerror = function () {
205
					if (this.imageCache[url]) {
206
						this.imageCache[url].reject(url);
207
					}
208
				}.bind(this);
209
				if (mimeType === 'image/svg+xml') {
210
					image.src = this._getSVG(url);
211
				} else {
212
					image.src = url;
213
				}
214
			}
215
			return this.imageCache[url];
216
		},
217
218
		/**
219
		 * Shows a new image in the slideshow and preloads the next in the list
220
		 *
221
		 * @param {number} current
222
		 * @param {Object} next
223
		 */
224
		next: function (current, next) {
225
			this.show(current).then(function () {
226
				// Preloads the next image in the list
227
				this.loadImage(next);
228
			}.bind(this));
229
		},
230
231
		/**
232
		 * Determines which colour to use for the background
233
		 *
234
		 * @param {*} image
235
		 * @param {string} mimeType
236
		 *
237
		 * @returns {string}
238
		 * @private
239
		 */
240
		_getBackgroundColour: function (image, mimeType) {
241
			var backgroundColour = this.darkBackgroundColour;
242
			if (this._isTransparent(mimeType) && this._isMainlyDark(image)) {
243
				backgroundColour = this.lightBackgroundColour;
244
			}
245
			return backgroundColour;
246
		},
247
248
		/**
249
		 * Calculates the luminance of an image to determine if an image is mainly dark
250
		 *
251
		 * @param {*} image
252
		 *
253
		 * @returns {boolean}
254
		 * @private
255
		 */
256
		_isMainlyDark: function (image) {
257
			var isMainlyDark = false;
258
			var numberOfSamples = 1000; // Seems to be the sweet spot
259
			// The name has to be 'canvas'
260
			var lumiCanvas = document.createElement('canvas');
261
262
			var imgArea = image.width * image.height;
263
			var canArea = numberOfSamples;
264
			var factor = Math.sqrt(canArea / imgArea);
265
266
			var scaledWidth = factor * image.width;
267
			var scaledHeight = factor * image.height;
268
			lumiCanvas.width = scaledWidth;
269
			lumiCanvas.height = scaledHeight;
270
			var lumiCtx = lumiCanvas.getContext('2d');
271
			lumiCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
272
			var imgData = lumiCtx.getImageData(0, 0, lumiCanvas.width, lumiCanvas.height);
273
			var pix = imgData.data; // pix.length will be approximately 4*numberOfSamples (for RGBA)
274
			var pixelArraySize = pix.length;
275
			var totalLuminance = 0;
276
			var sampleNumber = 1;
277
			var averageLuminance;
278
			var totalAlpha = 0;
279
			var alphaLevel;
280
			var red = 0;
281
			var green = 0;
282
			var blue = 0;
283
			var alpha = 0;
284
			var lum = 0;
285
			var alphaThreshold = 0.1;
286
287
			var sampleCounter = 0;
288
			var itemsPerPixel = 4; // red, green, blue, alpha
289
			// i += 4 because 4 colours for every pixel
290
			for (var i = 0, n = pixelArraySize; i < n; i += itemsPerPixel) {
291
				sampleCounter++;
292
				alpha = pix[i + 3] / 255;
293
				totalAlpha += alpha;
294
				if (Math.ceil(alpha * 100) / 100 > alphaThreshold) {
295
					red = pix[i];
296
					green = pix[i + 1];
297
					blue = pix[i + 2];
298
					// Luminance formula from
299
					// http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
300
					lum = (red + red + green + green + green + blue) / 6;
301
					//lum = (red * 0.299 + green * 0.587 + blue * 0.114 );
302
					totalLuminance += lum * alpha;
303
					sampleNumber++;
304
				}
305
			}
306
307
			// Deletes the canvas
308
			lumiCanvas = null;
309
310
			// Calculate the optimum background colour for this image
311
			averageLuminance = Math.ceil((totalLuminance / sampleNumber) * 100) / 100;
312
			alphaLevel = Math.ceil((totalAlpha / numberOfSamples) * 100);
313
314
			if (averageLuminance < 60 && alphaLevel < 90) {
315
				isMainlyDark = true;
316
			}
317
318
			return isMainlyDark;
319
		},
320
321
		/**
322
		 * Stops the slideshow
323
		 */
324
		stop: function () {
325
			this.active = false;
326
			this.images = null;
327
			this._hideImage();
328
			if (this.onStop) {
329
				this.onStop();
330
			}
331
		},
332
333
		/**
334
		 * Sends the current image as a download
335
		 *
336
		 * @param {string} downloadUrl
337
		 *
338
		 * @returns {boolean}
339
		 */
340
		getImageDownload: function (downloadUrl) {
341
			OC.redirect(downloadUrl);
342
			return false;
343
		},
344
345
		/**
346
		 * Changes the colour of the background of the image
347
		 */
348
		toggleBackground: function () {
349
			var toHex = function (x) {
350
				return ("0" + parseInt(x).toString(16)).slice(-2);
351
			};
352
			var container = this.zoomablePreviewContainer.children('img');
353
			var rgb = container.css('background-color').match(/\d+/g);
354
			var hex = "#" + toHex(rgb[0]) + toHex(rgb[1]) + toHex(rgb[2]);
355
			var $border = 30 / window.devicePixelRatio;
356
			var newBackgroundColor;
357
358
			// Grey #363636
359
			if (hex === this.darkBackgroundColour) {
360
				newBackgroundColor = this.lightBackgroundColour;
361
			} else {
362
				newBackgroundColor = this.darkBackgroundColour;
363
			}
364
365
			container.css('background-color', newBackgroundColor);
366
			if (this.backgroundToggle === true) {
367
				container.css('outline', $border + 'px solid ' + newBackgroundColor);
368
			}
369
		},
370
371
		/**
372
		 * Shows an error notification
373
		 *
374
		 * @param {string} message
375
		 */
376
		showErrorNotification: function (message) {
377
			if ($.isEmptyObject(message)) {
378
				message = t('gallery',
379
					'<strong>Error!</strong> Could not generate a preview of this file.<br>' +
380
					'Please go to the next slide while we remove this image from the slideshow');
381
			}
382
			this.container.find('.notification').html(message);
383
			this.container.find('.notification').show();
384
			this.controls.hideButton('.changeBackground');
385
		},
386
387
		/**
388
		 * Hides the error notification
389
		 */
390
		hideErrorNotification: function () {
391
			this.container.find('.notification').hide();
392
			this.container.find('.notification').html('');
393
		},
394
395
		/**
396
		 * Removes a specific button from the interface
397
		 *
398
		 * @param button
399
		 */
400
		removeButton: function (button) {
401
			this.controls.removeButton(button);
402
		},
403
404
		/**
405
		 * Deletes an image from the slideshow
406
		 *
407
		 * @param {object} image
408
		 * @param {number} currentIndex
409
		 */
410
		deleteImage: function (image, currentIndex) {
411
			// These are Gallery specific commands to be replaced
412
			// which should sit somewhere else
413
			if (!window.galleryFileAction) {
414
				delete Gallery.imageMap[image.path];
415
				delete Thumbnails.map[image.file];
416
				Gallery.albumMap[Gallery.currentAlbum].images.splice(currentIndex, 1);
417
				Gallery.view.init(Gallery.currentAlbum);
418
			}
419
		},
420
421
		/**
422
		 * Automatically fades the controls after 3 seconds
423
		 *
424
		 * @private
425
		 */
426
		_initControlsAutoFader: function () {
427
			var inactiveCallback = function () {
428
				this.container.addClass('inactive');
429
			}.bind(this);
430
			var inactiveTimeout = setTimeout(inactiveCallback, 3000);
431
432
			this.container.on('mousemove touchstart', function () {
433
				this.container.removeClass('inactive');
434
				clearTimeout(inactiveTimeout);
435
				inactiveTimeout = setTimeout(inactiveCallback, 3000);
436
			}.bind(this));
437
		},
438
439
		/**
440
		 * Simplest way to detect if image is transparent.
441
		 *
442
		 * That's very inaccurate since it doesn't include images which support transparency
443
		 *
444
		 * @param mimeType
445
		 * @returns {boolean}
446
		 * @private
447
		 */
448
		_isTransparent: function (mimeType) {
449
			return !(mimeType === 'image/jpeg'
450
				|| mimeType === 'image/x-dcraw'
451
				|| mimeType === 'application/font-sfnt'
452
				|| mimeType === 'application/x-font'
453
			);
454
		},
455
456
		/**
457
		 * Changes the browser Url, based on the current image
458
		 *
459
		 * @param {string} path
460
		 * @private
461
		 */
462
		_setUrl: function (path) {
463
			if (history && history.replaceState) {
464
				history.replaceState('', '', '#' + encodeURI(path));
465
			}
466
		},
467
468
		/**
469
		 * Hides the current image (before loading the next)
470
		 *
471
		 * @private
472
		 */
473
		_hideImage: function () {
474
			this.zoomablePreviewContainer.empty();
475
			this.controls.hideActionButtons();
476
		},
477
478
		/**
479
		 * Retrieves an SVG
480
		 *
481
		 * An SVG can't be simply attached to a src attribute like a bitmap image
482
		 *
483
		 * @param {string} source
484
		 *
485
		 * @returns {*}
486
		 * @private
487
		 */
488
		_getSVG: function (source) {
489
			var svgPreview = null;
490
			// DOMPurify only works with IE10+ and we load SVGs in the IMG tag
491
			if (window.btoa &&
492
				document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image",
493
					"1.1")) {
494
				var xmlHttp = new XMLHttpRequest();
495
				xmlHttp.open("GET", source, false);
496
				xmlHttp.send(null);
497
				if (xmlHttp.status === 200) {
498
					var pureSvg = DOMPurify.sanitize(xmlHttp.responseText, {ADD_TAGS: ['filter']});
499
					// Remove XML comment garbage left in the purified data
500
					var badTag = pureSvg.indexOf(']&gt;');
501
					var fixedPureSvg = pureSvg.substring(badTag < 0 ? 0 : 5, pureSvg.length);
502
					svgPreview = "data:image/svg+xml;base64," + window.btoa(fixedPureSvg);
503
				}
504
			}
505
506
			return svgPreview;
507
		},
508
509
		/**
510
		 * Retrieves the slideshow's template
511
		 *
512
		 * @returns {*}
513
		 * @private
514
		 */
515
		_getSlideshowTemplate: function () {
516
			var defer = $.Deferred();
517
			if (!this.$slideshowTemplate) {
518
				var self = this;
519
				var url = OC.generateUrl('apps/gallery/slideshow', null);
520
				$.get(url, function (tmpl) {
521
						var template = $(tmpl);
522
						var tmplButton;
523
						var buttonsArray = [
524
							{
525
								el: '.next',
526
								trans: t('gallery', 'Next')
527
							},
528
							{
529
								el: '.play',
530
								trans: t('gallery', 'Play'),
531
								toolTip: true
532
							},
533
							{
534
								el: '.pause',
535
								trans: t('gallery', 'Pause'),
536
								toolTip: true
537
							},
538
							{
539
								el: '.previous',
540
								trans: t('gallery', 'Previous')
541
							},
542
							{
543
								el: '.exit',
544
								trans: t('gallery', 'Close'),
545
								toolTip: true
546
							},
547
							{
548
								el: '.downloadImage',
549
								trans: t('gallery', 'Download'),
550
								toolTip: true
551
							},
552
							{
553
								el: '.changeBackground',
554
								trans: t('gallery', 'Toggle background'),
555
								toolTip: true
556
							},
557
							{
558
								el: '.deleteImage',
559
								trans: t('gallery', 'Delete'),
560
								toolTip: true
561
							},
562
							{
563
								el: '.shareImage',
564
								trans: t('gallery', 'Share'),
565
								toolTip: true
566
							}
567
						];
568
						for (var i = 0; i < buttonsArray.length; i++) {
569
							var button = buttonsArray[i];
570
571
							tmplButton = template.find(button.el);
572
							tmplButton.val(button.trans);
573
							if (button.toolTip) {
574
								tmplButton.attr("title", button.trans);
575
							}
576
						}
577
						self.$slideshowTemplate = template;
578
						defer.resolve(self.$slideshowTemplate);
579
					})
580
					.fail(function () {
581
						defer.reject();
582
					});
583
			} else {
584
				defer.resolve(this.$slideshowTemplate);
585
			}
586
			return defer.promise();
587
		}
588
	};
589
590
	window.SlideShow = SlideShow;
591
})(jQuery, OC, OCA, t);
592